From 2ffa7b70545939addf737aa66857f0ab6b007425 Mon Sep 17 00:00:00 2001 From: chechu Date: Mon, 30 Jun 2014 16:48:40 +0100 Subject: [PATCH 0001/2592] added info on bmi and vsi (owner field) --- SoftLayer/CLI/modules/server.py | 6 ++++-- SoftLayer/CLI/modules/vs.py | 6 +++++- SoftLayer/managers/hardware.py | 2 ++ SoftLayer/managers/vs.py | 2 ++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index b2e16852e..e5d5451bc 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -86,7 +86,8 @@ def execute(self, args): 'memory', 'primary_ip', 'backend_ip', - 'active_transaction' + 'active_transaction', + 'owner' ]) table.sortby = args.get('--sortby') or 'host' @@ -101,6 +102,7 @@ def execute(self, args): server['primaryIpAddress'] or blank(), server['primaryBackendIpAddress'] or blank(), active_txn(server), + server['billingItem']['orderItem']['order']['userRecord']['username'], ]) return table @@ -150,7 +152,7 @@ def execute(self, args): ['softwareDescription']['name'] or blank() )]) table.add_row(['created', result['provisionDate'] or blank()]) - + table.add_row(['owner', result ['billingItem']['orderItem']['order']['userRecord']['username']]) vlan_table = Table(['type', 'number', 'id']) for vlan in result['networkVlans']: vlan_table.add_row([ diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index d1e1a356e..db1f01d0a 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -95,7 +95,7 @@ def execute(self, args): table = Table([ 'id', 'datacenter', 'host', 'cores', 'memory', 'primary_ip', - 'backend_ip', 'active_transaction', + 'backend_ip', 'active_transaction','owner' ]) table.sortby = args.get('--sortby') or 'host' @@ -110,6 +110,7 @@ def execute(self, args): guest['primaryIpAddress'] or blank(), guest['primaryBackendIpAddress'] or blank(), active_txn(guest), + guest['billingItem']['orderItem']['order']['userRecord']['username'] ]) return table @@ -170,6 +171,9 @@ def execute(self, args): table.add_row(['private_cpu', result['dedicatedAccountHostOnlyFlag']]) table.add_row(['created', result['createDate']]) table.add_row(['modified', result['modifyDate']]) + table.add_row(['owner', FormattedItem( + lookup(result, 'billingItem', 'orderItem','order','userRecord','username'), + )]) vlan_table = Table(['type', 'number', 'id']) for vlan in result['networkVlans']: diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 9598d8ec3..cb01481fb 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -120,6 +120,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, 'primaryBackendIpAddress', 'primaryIpAddress', 'datacenter', + 'billingItem.orderItem.order.userRecord[username]', ] server_items = [ 'activeTransaction[id, transactionStatus[friendlyName,name]]', @@ -291,6 +292,7 @@ def get_hardware(self, hardware_id, **kwargs): 'hourlyBillingFlag', 'tagReferences[id,tag[name,id]]', 'networkVlans[id,vlanNumber,networkSpace]', + 'billingItem.orderItem.order.userRecord[username]', ] kwargs['mask'] = "mask[%s]" % ','.join(items) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 039d9460a..1eb325cc7 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -86,6 +86,7 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, 'datacenter', 'activeTransaction.transactionStatus[friendlyName,name]', 'status', + 'billingItem.orderItem.order.userRecord[username]' ] kwargs['mask'] = "mask[%s]" % ','.join(items) @@ -199,6 +200,7 @@ def get_instance(self, instance_id, **kwargs): 'billingItem.recurringFee', 'tagReferences[id,tag[name,id]]', 'networkVlans[id,vlanNumber,networkSpace]', + 'billingItem.orderItem.order.userRecord[username]' ] kwargs['mask'] = "mask[%s]" % ','.join(items) From 5709a7d35f97cfdecfe1430f6a0d9890a419f8a1 Mon Sep 17 00:00:00 2001 From: chechu Date: Mon, 7 Jul 2014 12:02:21 +0100 Subject: [PATCH 0002/2592] tox passed --- SoftLayer/CLI/modules/server.py | 8 ++++++-- SoftLayer/CLI/modules/vs.py | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index e5d5451bc..9574a3d63 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -102,7 +102,9 @@ def execute(self, args): server['primaryIpAddress'] or blank(), server['primaryBackendIpAddress'] or blank(), active_txn(server), - server['billingItem']['orderItem']['order']['userRecord']['username'], + server + ['billingItem']['orderItem']['order']['userRecord']['username'] + or blank(), ]) return table @@ -152,7 +154,9 @@ def execute(self, args): ['softwareDescription']['name'] or blank() )]) table.add_row(['created', result['provisionDate'] or blank()]) - table.add_row(['owner', result ['billingItem']['orderItem']['order']['userRecord']['username']]) + table.add_row(['owner', + result['billingItem']['orderItem']['order'] + ['userRecord']['username'] or blank()]) vlan_table = Table(['type', 'number', 'id']) for vlan in result['networkVlans']: vlan_table.add_row([ diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index db1f01d0a..365be8c48 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -95,7 +95,7 @@ def execute(self, args): table = Table([ 'id', 'datacenter', 'host', 'cores', 'memory', 'primary_ip', - 'backend_ip', 'active_transaction','owner' + 'backend_ip', 'active_transaction', 'owner' ]) table.sortby = args.get('--sortby') or 'host' @@ -110,7 +110,8 @@ def execute(self, args): guest['primaryIpAddress'] or blank(), guest['primaryBackendIpAddress'] or blank(), active_txn(guest), - guest['billingItem']['orderItem']['order']['userRecord']['username'] + guest['billingItem']['orderItem']['order'] + ['userRecord']['username'] ]) return table @@ -172,7 +173,7 @@ def execute(self, args): table.add_row(['created', result['createDate']]) table.add_row(['modified', result['modifyDate']]) table.add_row(['owner', FormattedItem( - lookup(result, 'billingItem', 'orderItem','order','userRecord','username'), + lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), )]) vlan_table = Table(['type', 'number', 'id']) From 7d5172357a24fd94443a047bdeeab57f893d93f9 Mon Sep 17 00:00:00 2001 From: chechu Date: Tue, 8 Jul 2014 17:08:53 +0100 Subject: [PATCH 0003/2592] error on orderItem --- SoftLayer/CLI/modules/server.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 9574a3d63..122725171 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -93,6 +93,10 @@ def execute(self, args): for server in servers: server = NestedDict(server) + if 'billingItem' in server: + if 'orderItem' in server['billingItem']: + user = (server['billingItem']['orderItem']['order'] + ['userRecord']['username']) table.add_row([ server['id'], server['datacenter']['name'] or blank(), @@ -102,9 +106,7 @@ def execute(self, args): server['primaryIpAddress'] or blank(), server['primaryBackendIpAddress'] or blank(), active_txn(server), - server - ['billingItem']['orderItem']['order']['userRecord']['username'] - or blank(), + user or blank(), ]) return table @@ -154,9 +156,12 @@ def execute(self, args): ['softwareDescription']['name'] or blank() )]) table.add_row(['created', result['provisionDate'] or blank()]) + if 'billingItem' in result: + if 'orderItem' in result['billingItem']: + user = (result['billingItem']['orderItem']['order'] + ['userRecord']['username']) table.add_row(['owner', - result['billingItem']['orderItem']['order'] - ['userRecord']['username'] or blank()]) + user or blank()]) vlan_table = Table(['type', 'number', 'id']) for vlan in result['networkVlans']: vlan_table.add_row([ From c9d9c363c3992998fdf8430e6b0bfaf1e58f482f Mon Sep 17 00:00:00 2001 From: chechu Date: Tue, 8 Jul 2014 20:47:52 +0100 Subject: [PATCH 0004/2592] test added but fail --- SoftLayer/CLI/modules/server.py | 2 + SoftLayer/tests/CLI/modules/server_tests.py | 3 +- SoftLayer/tests/CLI/modules/vs_tests.py | 9 ++-- SoftLayer/tests/fixtures/Account.py | 49 +++++++++++++++++++-- SoftLayer/tests/fixtures/Hardware_Server.py | 10 ++++- SoftLayer/tests/fixtures/Virtual_Guest.py | 9 ++++ 6 files changed, 74 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 122725171..cf83c812f 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -93,6 +93,7 @@ def execute(self, args): for server in servers: server = NestedDict(server) + user = None if 'billingItem' in server: if 'orderItem' in server['billingItem']: user = (server['billingItem']['orderItem']['order'] @@ -156,6 +157,7 @@ def execute(self, args): ['softwareDescription']['name'] or blank() )]) table.add_row(['created', result['provisionDate'] or blank()]) + user = None if 'billingItem' in result: if 'orderItem' in result['billingItem']: user = (result['billingItem']['orderItem']['order'] diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index 7186914d6..a5d602f41 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -228,7 +228,8 @@ def test_server_details(self): 'tags': ['test_tag'], 'users': ['root abc123'], 'vlans': [{'id': 9653, 'number': 1800, 'type': 'PRIVATE'}, - {'id': 19082, 'number': 3672, 'type': 'PUBLIC'}] + {'id': 19082, 'number': 3672, 'type': 'PUBLIC'}], + 'owner': 'chechu' } self.assertEqual(expected, format_output(output, 'python')) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 96bd59cf6..f09561c6c 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -26,7 +26,8 @@ def test_list_vs(self): 'cores': 2, 'active_transaction': None, 'id': 100, - 'backend_ip': '10.45.19.37'}, + 'backend_ip': '10.45.19.37', + 'owner': 'chechu' }, {'datacenter': 'TEST00', 'primary_ip': '172.16.240.7', 'host': 'vs-test2.test.sftlyr.ws', @@ -34,7 +35,8 @@ def test_list_vs(self): 'cores': 4, 'active_transaction': None, 'id': 104, - 'backend_ip': '10.45.19.35'}], + 'backend_ip': '10.45.19.35', + 'owner': 'chechu' }], format_output(output, 'python')) def test_detail_vs(self): @@ -67,7 +69,8 @@ def test_detail_vs(self): 'users': [{'password': 'pass', 'username': 'user'}], 'vlans': [{'type': 'PUBLIC', 'number': 23, - 'id': 1}]}, + 'id': 1}], + 'owner': 'chechu'}, format_output(output, 'python')) def test_create_options(self): diff --git a/SoftLayer/tests/fixtures/Account.py b/SoftLayer/tests/fixtures/Account.py index 1c8d8ebae..1de74d1bf 100644 --- a/SoftLayer/tests/fixtures/Account.py +++ b/SoftLayer/tests/fixtures/Account.py @@ -33,6 +33,16 @@ 'globalIdentifier': '1a2b3c-1701', 'primaryBackendIpAddress': '10.45.19.37', 'hourlyBillingFlag': False, + + 'billingItem': {'id': 6327, 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, }, { 'id': 104, 'hostname': 'vs-test2', @@ -49,6 +59,15 @@ 'globalIdentifier': '05a8ac-6abf0', 'primaryBackendIpAddress': '10.45.19.35', 'hourlyBillingFlag': True, + 'billingItem': {'id': 6327, 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, }] getMonthlyVirtualGuests = [vs for vs in getVirtualGuests @@ -61,7 +80,15 @@ 'id': 1000, 'datacenter': {'id': 50, 'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 6327, 'recurringFee': 1.54}, + 'billingItem': {'id': 6327, 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.1.100', 'hostname': 'hardware-test1', 'domain': 'test.sftlyr.ws', @@ -110,7 +137,15 @@ 'id': 1001, 'datacenter': {'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 7112}, + 'billingItem': {'id': 7112, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.4.94', 'hostname': 'hardware-test2', 'domain': 'test.sftlyr.ws', @@ -144,7 +179,15 @@ 'id': 1002, 'datacenter': {'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 7112}, + 'billingItem': {'id': 7112, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.4.95', 'hostname': 'hardware-bad-memory', 'domain': 'test.sftlyr.ws', diff --git a/SoftLayer/tests/fixtures/Hardware_Server.py b/SoftLayer/tests/fixtures/Hardware_Server.py index 9e4f69325..71e4626e6 100644 --- a/SoftLayer/tests/fixtures/Hardware_Server.py +++ b/SoftLayer/tests/fixtures/Hardware_Server.py @@ -2,7 +2,15 @@ 'id': 1000, 'datacenter': {'id': 50, 'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 6327, 'recurringFee': 1.54}, + 'billingItem': {'id': 6327, 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.1.100', 'hostname': 'hardware-test1', 'domain': 'test.sftlyr.ws', diff --git a/SoftLayer/tests/fixtures/Virtual_Guest.py b/SoftLayer/tests/fixtures/Virtual_Guest.py index ce24df919..be74621bc 100644 --- a/SoftLayer/tests/fixtures/Virtual_Guest.py +++ b/SoftLayer/tests/fixtures/Virtual_Guest.py @@ -4,6 +4,15 @@ 'domain': 'test.sftlyr.ws', 'fullyQualifiedDomainName': 'vs-test1.test.sftlyr.ws', 'status': {'keyName': 'ACTIVE', 'name': 'Active'}, + 'billingItem': {'id': 6327, 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'datacenter': {'id': 50, 'name': 'TEST00', 'description': 'Test Data Center'}, 'powerState': {'keyName': 'RUNNING', 'name': 'Running'}, From 8bb98ca3e17e5cfd7395fd8235a37dcbf9a82d99 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 22 Jul 2014 10:48:56 -0500 Subject: [PATCH 0005/2592] Converts the integer IDs to strings to be printed correctly --- SoftLayer/CLI/modules/metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index 82539d2de..07f6c12f3 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -80,7 +80,7 @@ class DatacenterId(MetaRunnable): action = 'datacenter_id' def _execute(self, _): - return SoftLayer.MetadataManager().get('datacenter_id') + return str(SoftLayer.MetadataManager().get('datacenter_id')) class FrontendMacAddresses(MetaRunnable): @@ -129,7 +129,7 @@ class Id(MetaRunnable): action = 'id' def _execute(self, _): - return SoftLayer.MetadataManager().get('id') + return str(SoftLayer.MetadataManager().get('id')) class PrimaryBackendIpAddress(MetaRunnable): From b09db16b94742f9b0ce484fcb43124c012003562 Mon Sep 17 00:00:00 2001 From: boden Date: Wed, 23 Jul 2014 14:08:03 -0400 Subject: [PATCH 0006/2592] tag support for vs create / edit Adds the ability to specify tags both on the vs (aka cci) create instance CLI / call as well as modify tags on the edit CLI / call. Additionally this PR supports tags w/r/t exporting templates and create using the --like option. Finally unit tests are included. implements https://github.com/softlayer/softlayer-python/issues/354 --- SoftLayer/CLI/modules/vs.py | 12 ++++++++++ SoftLayer/managers/vs.py | 26 ++++++++++++++++++--- SoftLayer/testing/fixtures/Virtual_Guest.py | 4 ++-- SoftLayer/tests/CLI/modules/vs_tests.py | 3 ++- SoftLayer/tests/managers/vs_tests.py | 16 +++++++++++-- SoftLayer/utils.py | 17 ++++++++++++++ 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index f868bd32c..05f421eb6 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -366,6 +366,7 @@ class CreateVS(environment.CLIRunnable): --test Do not create VS, just get a quote --export=FILE Exports options to a template file -F, --userfile=FILE Read userdata from file + -g --tag=TAG Comma list of tags to set or empty string to remove all -i, --postinstall=URI Post-install script to download (Only HTTPS executes, HTTP leaves file in /root) -k, --key=KEY SSH keys to add to the root user. Can be specified @@ -533,6 +534,12 @@ def _update_with_like_args(self, args): '--private': like_details['privateNetworkOnlyFlag'], } + tag_refs = like_details.get('tagReferences', None) + if tag_refs is not None and len(tag_refs) > 0: + tags = ','.join([t['tag']['name'] for t in tag_refs]) + like_args['--tag'] = tags + + # Handle mutually exclusive options like_image = utils.lookup(like_details, 'blockDeviceTemplateGroup', @@ -628,6 +635,9 @@ def _parse_create_args(self, args): if args.get('--vlan_private'): data['private_vlan'] = args['--vlan_private'] + if args.get('--tag'): + data['tag'] = args['--tag'] + return data @@ -970,6 +980,7 @@ class EditVS(environment.CLIRunnable): Options: -D --domain=DOMAIN Domain portion of the FQDN example: example.com -F --userfile=FILE Read userdata from file + -g --tag=TAG Comma list of tags to set or empty string to remove all -H --hostname=HOST Host portion of the FQDN. example: server -u --userdata=DATA User defined metadata string """ @@ -995,6 +1006,7 @@ def execute(self, args): data['hostname'] = args.get('--hostname') data['domain'] = args.get('--domain') + data['tag'] = args.get("--tag") vsi = SoftLayer.VSManager(self.client) vs_id = helpers.resolve_id(vsi.resolve_ids, diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 708376818..21c76e4f7 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -452,8 +452,14 @@ def create_instance(self, **kwargs): access to the private network. Defaults to false :param list ssh_keys: The SSH keys to add to the root user :param int nic_speed: The port speed to set + :param string tag: tags to set on the VS as a comma separated list. + Use the empty string to remove all tags. """ - return self.guest.createObject(self._generate_create_dict(**kwargs)) + tag, = utils.dict_extract(kwargs, {'tag': None}) + inst = self.guest.createObject(self._generate_create_dict(**kwargs)) + if tag is not None: + self.guest.setTags(tag, id=inst['id']) + return inst def create_instances(self, config_list): """ Creates multiple virtual server instances @@ -461,9 +467,18 @@ def create_instances(self, config_list): This takes a list of dictionaries using the same arguments as create_instance(). """ - return self.guest.createObjects([self._generate_create_dict(**kwargs) + tags = [utils.dict_extract(conf, {'tag': None})[0] + for conf in config_list] + + resp = self.guest.createObjects([self._generate_create_dict(**kwargs) for kwargs in config_list]) + for index in range(0, len(resp)): + if tags[index] is not None: + self.guest.setTags(tags[index], id=resp[index]['id']) + + return resp + def change_port_speed(self, instance_id, public, speed): """ Allows you to change the port speed of a virtual server's NICs. @@ -503,7 +518,7 @@ def _get_ids_from_ip(self, ip_address): return [result['id'] for result in results] def edit(self, instance_id, userdata=None, hostname=None, domain=None, - notes=None): + notes=None, tag=None): """ Edit hostname, domain name, notes, and/or the user data of a VS Parameters set to None will be ignored and not attempted to be updated. @@ -514,6 +529,8 @@ def edit(self, instance_id, userdata=None, hostname=None, domain=None, :param string hostname: valid hostname :param string domain: valid domain namem :param string notes: notes about this particular VS + :param string tag: tags to set on the VS as a comma separated list. + Use the empty string to remove all tags. """ @@ -521,6 +538,9 @@ def edit(self, instance_id, userdata=None, hostname=None, domain=None, if userdata: self.guest.setUserMetadata([userdata], id=instance_id) + if tag is not None: + self.guest.setTags(tag, id=instance_id) + if hostname: obj['hostname'] = hostname diff --git a/SoftLayer/testing/fixtures/Virtual_Guest.py b/SoftLayer/testing/fixtures/Virtual_Guest.py index 4323882d5..429549908 100644 --- a/SoftLayer/testing/fixtures/Virtual_Guest.py +++ b/SoftLayer/testing/fixtures/Virtual_Guest.py @@ -218,9 +218,9 @@ setPrivateNetworkInterfaceSpeed = True setPublicNetworkInterfaceSpeed = True createObject = getObject -createObjects = True +createObjects = [getObject] generateOrderTemplate = {} setUserMetadata = ['meta'] reloadOperatingSystem = 'OK' - +setTags = True createArchiveTransaction = {} diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 2fbb6b61d..a479cf586 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -124,7 +124,8 @@ def test_create(self, confirm_mock): '--vlan_public': None, '--vlan_private': None, '--wait': None, - '--really': False}) + '--really': False, + '--tag': 'dev,green'}) self.assertEqual([{'guid': '1a2b3c-1701', 'id': 100, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 46576d69a..89216d7f3 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -137,16 +137,19 @@ def test_create_verify(self, create_dict): @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') def test_create_instance(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} - self.vs.create_instance(test=1, verify=1) + self.vs.create_instance(test=1, verify=1, tag='dev,green') create_dict.assert_called_once_with(test=1, verify=1) self.client['Virtual_Guest'].createObject.assert_called_once_with( {'test': 1, 'verify': 1}) + self.client['Virtual_Guest'].setTags.assert_called_once_with( + 'dev,green', id=100) def test_create_instances(self): self.vs.create_instances([{'cpus': 1, 'memory': 1024, 'hostname': 'server', - 'domain': 'example.com'}]) + 'domain': 'example.com', + 'tag': 'dev,green'}]) self.client['Virtual_Guest'].createObjects.assert_called_once_with([ {'domain': 'example.com', 'hourlyBillingFlag': True, @@ -154,6 +157,8 @@ def test_create_instances(self): 'maxMemory': 1024, 'hostname': 'server', 'startCpus': 1}]) + self.client['Virtual_Guest'].setTags.assert_called_once_with( + 'dev,green', id=100) def test_generate_os_and_image(self): self.assertRaises( @@ -532,6 +537,13 @@ def test_edit(self): self.vs.edit(100, **args) service.editObject.assert_called_once_with(args, id=100) + # Test tag support + self.vs.edit(100, tag='dev,green') + service.setTags.assert_called_once_with('dev,green', id=100) + service.setTags.reset_mock() + self.vs.edit(100, tag='') + service.setTags.assert_called_once_with('', id=100) + def test_captures(self): archive = self.client['Virtual_Guest'].createArchiveTransaction diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index e0d28239d..84b58946d 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -156,3 +156,20 @@ def resolve_ids(identifier, resolvers): return ids return [] + + +def dict_extract(dictionary, keys): + """Extracts & removes keys from a dict with a default value. + + :param dict dictionary: the target dictionary to operate on + :param dict keys: a dict who's keys specify the keys to extract + from the dictionary and values are the defaults to use if + the key is not in the target dict. + :returns tuple + """ + vals = [] + for key, default in keys.iteritems(): + vals.append(dictionary.get(key, default)) + if dictionary.get(key, None) is not None: + del dictionary[key] + return tuple(vals) From 62d1bcc0abaa902c4f8c0b3491037e4ab2f5fc94 Mon Sep 17 00:00:00 2001 From: boden Date: Wed, 23 Jul 2014 14:44:58 -0400 Subject: [PATCH 0007/2592] vm tag support updates Address code review comments as well as failing CI tests. --- SoftLayer/CLI/modules/vs.py | 3 +-- SoftLayer/managers/vs.py | 9 ++++----- SoftLayer/utils.py | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 05f421eb6..502253ed5 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -366,7 +366,7 @@ class CreateVS(environment.CLIRunnable): --test Do not create VS, just get a quote --export=FILE Exports options to a template file -F, --userfile=FILE Read userdata from file - -g --tag=TAG Comma list of tags to set or empty string to remove all + -g --tag=TAG Comma list of tags to set -i, --postinstall=URI Post-install script to download (Only HTTPS executes, HTTP leaves file in /root) -k, --key=KEY SSH keys to add to the root user. Can be specified @@ -538,7 +538,6 @@ def _update_with_like_args(self, args): if tag_refs is not None and len(tag_refs) > 0: tags = ','.join([t['tag']['name'] for t in tag_refs]) like_args['--tag'] = tags - # Handle mutually exclusive options like_image = utils.lookup(like_details, diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 21c76e4f7..13c757ee5 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -452,8 +452,7 @@ def create_instance(self, **kwargs): access to the private network. Defaults to false :param list ssh_keys: The SSH keys to add to the root user :param int nic_speed: The port speed to set - :param string tag: tags to set on the VS as a comma separated list. - Use the empty string to remove all tags. + :param string tag: tags to set on the VS as a comma separated list """ tag, = utils.dict_extract(kwargs, {'tag': None}) inst = self.guest.createObject(self._generate_create_dict(**kwargs)) @@ -473,9 +472,9 @@ def create_instances(self, config_list): resp = self.guest.createObjects([self._generate_create_dict(**kwargs) for kwargs in config_list]) - for index in range(0, len(resp)): - if tags[index] is not None: - self.guest.setTags(tags[index], id=resp[index]['id']) + for instance, tag in zip(resp, tags): + if tag is not None: + self.guest.setTags(tag, id=instance['id']) return resp diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 84b58946d..0162dc868 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -165,10 +165,10 @@ def dict_extract(dictionary, keys): :param dict keys: a dict who's keys specify the keys to extract from the dictionary and values are the defaults to use if the key is not in the target dict. - :returns tuple + :returns tuple """ vals = [] - for key, default in keys.iteritems(): + for key, default in keys.items(): vals.append(dictionary.get(key, default)) if dictionary.get(key, None) is not None: del dictionary[key] From d0a4042c455e3806b605e0daf94fc8aa7b557d71 Mon Sep 17 00:00:00 2001 From: Hans Kristian Moen Date: Fri, 25 Jul 2014 15:15:19 +0100 Subject: [PATCH 0008/2592] Add support for custom user agent string --- SoftLayer/API.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 0f1952882..e5569f8ae 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -44,6 +44,8 @@ class Client(object): :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. Example: `BasicAuthentication` :param config_file: A path to a configuration file used to load settings + :param user_agent: an optional User Agent to report when making API + calls if you wish to bypass the packages built in User Agent string Usage: @@ -57,7 +59,7 @@ class Client(object): _prefix = "SoftLayer_" def __init__(self, username=None, api_key=None, endpoint_url=None, - timeout=None, auth=None, config_file=None, proxy=None): + timeout=None, auth=None, config_file=None, proxy=None, user_agent=None): settings = config.get_client_settings(username=username, api_key=api_key, @@ -75,6 +77,7 @@ def __init__(self, username=None, api_key=None, endpoint_url=None, self.proxy = None if settings.get('proxy'): self.proxy = settings.get('proxy') + self.user_agent = user_agent def authenticate_with_password(self, username, password, security_question_id=None, @@ -162,7 +165,7 @@ def call(self, service, method, *args, **kwargs): } http_headers = { - 'User-Agent': consts.USER_AGENT, + 'User-Agent': self.user_agent or consts.USER_AGENT, 'Content-Type': 'application/xml', } From e48aa080e553209b48627ad8812bf787340afa40 Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Fri, 25 Jul 2014 15:19:48 -0500 Subject: [PATCH 0009/2592] The upgrade function errors out a lot due to a timeout. The timeout was happening during the _get_package_items function due to the object mask. I tweaked the mask and now the getItems call takes a few seconds rather than a few minutes. The response structure is slightly different, so I had to modify _get_item_id_for_upgrade to account for that. --- SoftLayer/managers/vs.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 708376818..01bf0c42e 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -610,7 +610,7 @@ def _get_package_items(self): """ Following Method gets all the item ids related to VS """ - mask = "mask[description,capacity,prices.id,categories[name,id]]" + mask = "mask[description,capacity,prices[id,categories[name,id]]]" package_type = "VIRTUAL_SERVER_INSTANCE" package_id = self.ordering_manager.get_package_id_by_type(package_type) package_service = self.ordering_manager.get_package_service() @@ -628,8 +628,9 @@ def _get_item_id_for_upgrade(self, package_items, option, value, """ vs_id = {'memory': 3, 'cpus': 80, 'nic_speed': 26} for item in package_items: - for j in range(len(item['categories'])): - if not (item['categories'][j]['id'] == vs_id[option] and + categories = item['prices'][0]['categories'] + for j in range(len(categories)): + if not (categories[j]['id'] == vs_id[option] and item['capacity'] == str(value)): continue if option == 'cpus': From 43a0ff999d42a2c44e86a7bca7d1f49e9acebec9 Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Mon, 28 Jul 2014 10:47:19 -0500 Subject: [PATCH 0010/2592] Modified the vs_tests and Product_Package to match the new result set returned by getItems. Categories are now under 'prices'. Before they were under 'item'. --- SoftLayer/testing/fixtures/Product_Package.py | 20 +++++++++---------- SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/testing/fixtures/Product_Package.py b/SoftLayer/testing/fixtures/Product_Package.py index e837298d4..f2621766e 100644 --- a/SoftLayer/testing/fixtures/Product_Package.py +++ b/SoftLayer/testing/fixtures/Product_Package.py @@ -1037,43 +1037,43 @@ def get_bmc_categories_mock(): getItems = [ { 'id': 1234, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 1122}], + 'prices': [{'id': 1122, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 2233, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 4477}], + 'prices': [{'id': 4477, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 1239, - 'categories': [{'id': 3, 'name': 'RAM'}], 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, - 'prices': [{'id': 1133}], + 'prices': [{'id': 1133, + 'categories': [{'id': 3, 'name': 'RAM'}]}], }, { 'id': 1240, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Private Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1007}], + 'prices': [{'id': 1007, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 1250, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1144}], + 'prices': [{'id': 1144, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 4439, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 46576d69a..60238d0d8 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -574,7 +574,7 @@ def test_get_item_id_for_upgrade(self): item_id = 0 package_items = self.client['Product_Package'].getItems(id=46) for item in package_items: - if ((item['categories'][0]['id'] == 3) + if ((item['prices'][0]['categories'][0]['id'] == 3) and (item.get('capacity') == '2')): item_id = item['prices'][0]['id'] break From 19471ad2075eff2f8ad4b35392cee2ff335b9d27 Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Mon, 28 Jul 2014 10:50:23 -0500 Subject: [PATCH 0011/2592] Revert "Modified the vs_tests and Product_Package to match the new result set returned by getItems. Categories are now under 'prices'. Before they were under 'item'." This reverts commit 43a0ff999d42a2c44e86a7bca7d1f49e9acebec9. --- SoftLayer/testing/fixtures/Product_Package.py | 20 +++++++++---------- SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/testing/fixtures/Product_Package.py b/SoftLayer/testing/fixtures/Product_Package.py index f2621766e..e837298d4 100644 --- a/SoftLayer/testing/fixtures/Product_Package.py +++ b/SoftLayer/testing/fixtures/Product_Package.py @@ -1037,43 +1037,43 @@ def get_bmc_categories_mock(): getItems = [ { 'id': 1234, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 1122, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], + 'prices': [{'id': 1122}], }, { 'id': 2233, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 4477, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], + 'prices': [{'id': 4477}], }, { 'id': 1239, + 'categories': [{'id': 3, 'name': 'RAM'}], 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, - 'prices': [{'id': 1133, - 'categories': [{'id': 3, 'name': 'RAM'}]}], + 'prices': [{'id': 1133}], }, { 'id': 1240, + 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Private Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1007, - 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], + 'prices': [{'id': 1007}], }, { 'id': 1250, + 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1144, - 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], + 'prices': [{'id': 1144}], }, { 'id': 4439, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 60238d0d8..46576d69a 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -574,7 +574,7 @@ def test_get_item_id_for_upgrade(self): item_id = 0 package_items = self.client['Product_Package'].getItems(id=46) for item in package_items: - if ((item['prices'][0]['categories'][0]['id'] == 3) + if ((item['categories'][0]['id'] == 3) and (item.get('capacity') == '2')): item_id = item['prices'][0]['id'] break From f5e61790ca2d5f4503884220a563a9c9a6b5b3d4 Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Mon, 28 Jul 2014 10:52:36 -0500 Subject: [PATCH 0012/2592] Modified vs_tests and Product_Package to use the new result set returned by getItems. Categories are now under 'prices'. Before they were under 'item'. --- SoftLayer/testing/fixtures/Product_Package.py | 20 +++++++++---------- SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/testing/fixtures/Product_Package.py b/SoftLayer/testing/fixtures/Product_Package.py index e837298d4..f2621766e 100644 --- a/SoftLayer/testing/fixtures/Product_Package.py +++ b/SoftLayer/testing/fixtures/Product_Package.py @@ -1037,43 +1037,43 @@ def get_bmc_categories_mock(): getItems = [ { 'id': 1234, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 1122}], + 'prices': [{'id': 1122, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 2233, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 4477}], + 'prices': [{'id': 4477, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 1239, - 'categories': [{'id': 3, 'name': 'RAM'}], 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, - 'prices': [{'id': 1133}], + 'prices': [{'id': 1133, + 'categories': [{'id': 3, 'name': 'RAM'}]}], }, { 'id': 1240, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Private Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1007}], + 'prices': [{'id': 1007, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 1250, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1144}], + 'prices': [{'id': 1144, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 4439, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 46576d69a..60238d0d8 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -574,7 +574,7 @@ def test_get_item_id_for_upgrade(self): item_id = 0 package_items = self.client['Product_Package'].getItems(id=46) for item in package_items: - if ((item['categories'][0]['id'] == 3) + if ((item['prices'][0]['categories'][0]['id'] == 3) and (item.get('capacity') == '2')): item_id = item['prices'][0]['id'] break From b10d8681605ae5ca877fed4fe79bee419fdb1e31 Mon Sep 17 00:00:00 2001 From: Hans Kristian Moen Date: Mon, 28 Jul 2014 16:58:00 +0100 Subject: [PATCH 0013/2592] Add support for custom HTTP User-Agent string --- SoftLayer/API.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index e5569f8ae..61851a35a 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -44,7 +44,7 @@ class Client(object): :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. Example: `BasicAuthentication` :param config_file: A path to a configuration file used to load settings - :param user_agent: an optional User Agent to report when making API + :param user_agent: an optional User Agent to report when making API calls if you wish to bypass the packages built in User Agent string Usage: @@ -59,7 +59,8 @@ class Client(object): _prefix = "SoftLayer_" def __init__(self, username=None, api_key=None, endpoint_url=None, - timeout=None, auth=None, config_file=None, proxy=None, user_agent=None): + timeout=None, auth=None, config_file=None, proxy=None, + user_agent=None): settings = config.get_client_settings(username=username, api_key=api_key, From 8e2cb794aadc5c93490aebf9f1cd5268c8e900f0 Mon Sep 17 00:00:00 2001 From: Hans Kristian Moen Date: Mon, 28 Jul 2014 16:58:00 +0100 Subject: [PATCH 0014/2592] Remove trailing whitespace and break long line in last patch --- SoftLayer/API.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index e5569f8ae..61851a35a 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -44,7 +44,7 @@ class Client(object): :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. Example: `BasicAuthentication` :param config_file: A path to a configuration file used to load settings - :param user_agent: an optional User Agent to report when making API + :param user_agent: an optional User Agent to report when making API calls if you wish to bypass the packages built in User Agent string Usage: @@ -59,7 +59,8 @@ class Client(object): _prefix = "SoftLayer_" def __init__(self, username=None, api_key=None, endpoint_url=None, - timeout=None, auth=None, config_file=None, proxy=None, user_agent=None): + timeout=None, auth=None, config_file=None, proxy=None, + user_agent=None): settings = config.get_client_settings(username=username, api_key=api_key, From 302d2e7725bd4e16724e7fec05805815a41a9f6e Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Mon, 28 Jul 2014 10:52:36 -0500 Subject: [PATCH 0015/2592] Changed tests to match _get_package_items Modified vs_tests and Product_Package to use the new result set returned by getItems. Categories are now under 'prices'. Before they were under 'item'. --- SoftLayer/testing/fixtures/Product_Package.py | 20 +++++++++---------- SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/testing/fixtures/Product_Package.py b/SoftLayer/testing/fixtures/Product_Package.py index e837298d4..f2621766e 100644 --- a/SoftLayer/testing/fixtures/Product_Package.py +++ b/SoftLayer/testing/fixtures/Product_Package.py @@ -1037,43 +1037,43 @@ def get_bmc_categories_mock(): getItems = [ { 'id': 1234, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 1122}], + 'prices': [{'id': 1122, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 2233, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 4477}], + 'prices': [{'id': 4477, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 1239, - 'categories': [{'id': 3, 'name': 'RAM'}], 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, - 'prices': [{'id': 1133}], + 'prices': [{'id': 1133, + 'categories': [{'id': 3, 'name': 'RAM'}]}], }, { 'id': 1240, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Private Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1007}], + 'prices': [{'id': 1007, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 1250, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1144}], + 'prices': [{'id': 1144, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 4439, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 46576d69a..60238d0d8 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -574,7 +574,7 @@ def test_get_item_id_for_upgrade(self): item_id = 0 package_items = self.client['Product_Package'].getItems(id=46) for item in package_items: - if ((item['categories'][0]['id'] == 3) + if ((item['prices'][0]['categories'][0]['id'] == 3) and (item.get('capacity') == '2')): item_id = item['prices'][0]['id'] break From 157eecdf5a7763eb02f91bd67382a9dd1ef90527 Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Mon, 28 Jul 2014 16:49:51 -0500 Subject: [PATCH 0016/2592] Revert "Changed tests to match _get_package_items Modified vs_tests and Product_Package to use the new result set returned by getItems. Categories are now under 'prices'. Before they were under 'item'." This reverts commit 302d2e7725bd4e16724e7fec05805815a41a9f6e. --- SoftLayer/testing/fixtures/Product_Package.py | 20 +++++++++---------- SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/testing/fixtures/Product_Package.py b/SoftLayer/testing/fixtures/Product_Package.py index f2621766e..e837298d4 100644 --- a/SoftLayer/testing/fixtures/Product_Package.py +++ b/SoftLayer/testing/fixtures/Product_Package.py @@ -1037,43 +1037,43 @@ def get_bmc_categories_mock(): getItems = [ { 'id': 1234, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 1122, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], + 'prices': [{'id': 1122}], }, { 'id': 2233, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 4477, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], + 'prices': [{'id': 4477}], }, { 'id': 1239, + 'categories': [{'id': 3, 'name': 'RAM'}], 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, - 'prices': [{'id': 1133, - 'categories': [{'id': 3, 'name': 'RAM'}]}], + 'prices': [{'id': 1133}], }, { 'id': 1240, + 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Private Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1007, - 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], + 'prices': [{'id': 1007}], }, { 'id': 1250, + 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1144, - 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], + 'prices': [{'id': 1144}], }, { 'id': 4439, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 60238d0d8..46576d69a 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -574,7 +574,7 @@ def test_get_item_id_for_upgrade(self): item_id = 0 package_items = self.client['Product_Package'].getItems(id=46) for item in package_items: - if ((item['prices'][0]['categories'][0]['id'] == 3) + if ((item['categories'][0]['id'] == 3) and (item.get('capacity') == '2')): item_id = item['prices'][0]['id'] break From d145d5acb530e0235c7298b450eb76322562ec0f Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Mon, 28 Jul 2014 16:52:06 -0500 Subject: [PATCH 0017/2592] Changed tests to match _get_package_items --- SoftLayer/testing/fixtures/Product_Package.py | 20 +++++++++---------- SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/testing/fixtures/Product_Package.py b/SoftLayer/testing/fixtures/Product_Package.py index e837298d4..f2621766e 100644 --- a/SoftLayer/testing/fixtures/Product_Package.py +++ b/SoftLayer/testing/fixtures/Product_Package.py @@ -1037,43 +1037,43 @@ def get_bmc_categories_mock(): getItems = [ { 'id': 1234, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 1122}], + 'prices': [{'id': 1122, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 2233, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 4477}], + 'prices': [{'id': 4477, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 1239, - 'categories': [{'id': 3, 'name': 'RAM'}], 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, - 'prices': [{'id': 1133}], + 'prices': [{'id': 1133, + 'categories': [{'id': 3, 'name': 'RAM'}]}], }, { 'id': 1240, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Private Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1007}], + 'prices': [{'id': 1007, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 1250, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1144}], + 'prices': [{'id': 1144, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 4439, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 46576d69a..60238d0d8 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -574,7 +574,7 @@ def test_get_item_id_for_upgrade(self): item_id = 0 package_items = self.client['Product_Package'].getItems(id=46) for item in package_items: - if ((item['categories'][0]['id'] == 3) + if ((item['prices'][0]['categories'][0]['id'] == 3) and (item.get('capacity') == '2')): item_id = item['prices'][0]['id'] break From b4f8ef0a35576049152ac06c4322fbd59d320c34 Mon Sep 17 00:00:00 2001 From: Robert Chumbley Date: Mon, 28 Jul 2014 16:52:06 -0500 Subject: [PATCH 0018/2592] Changed tests to match _get_package_items --- SoftLayer/testing/fixtures/Product_Package.py | 20 +++++++++---------- SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/testing/fixtures/Product_Package.py b/SoftLayer/testing/fixtures/Product_Package.py index e837298d4..f2621766e 100644 --- a/SoftLayer/testing/fixtures/Product_Package.py +++ b/SoftLayer/testing/fixtures/Product_Package.py @@ -1037,43 +1037,43 @@ def get_bmc_categories_mock(): getItems = [ { 'id': 1234, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 1122}], + 'prices': [{'id': 1122, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 2233, - 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}], 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, - 'prices': [{'id': 4477}], + 'prices': [{'id': 4477, + 'categories': [{'id': 26, 'name': 'Uplink Port Speeds'}]}], }, { 'id': 1239, - 'categories': [{'id': 3, 'name': 'RAM'}], 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, - 'prices': [{'id': 1133}], + 'prices': [{'id': 1133, + 'categories': [{'id': 3, 'name': 'RAM'}]}], }, { 'id': 1240, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Private Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1007}], + 'prices': [{'id': 1007, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 1250, - 'categories': [{'id': 80, 'name': 'Computing Instance'}], 'capacity': '4', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, - 'prices': [{'id': 1144}], + 'prices': [{'id': 1144, + 'categories': [{'id': 80, 'name': 'Computing Instance'}]}], }, { 'id': 4439, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 46576d69a..60238d0d8 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -574,7 +574,7 @@ def test_get_item_id_for_upgrade(self): item_id = 0 package_items = self.client['Product_Package'].getItems(id=46) for item in package_items: - if ((item['categories'][0]['id'] == 3) + if ((item['prices'][0]['categories'][0]['id'] == 3) and (item.get('capacity') == '2')): item_id = item['prices'][0]['id'] break From c895f49ac6cd2d98ae1ef38e8d026ad7ba883923 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 29 Jul 2014 09:17:26 -0500 Subject: [PATCH 0019/2592] Fix for pep8 CI failures --- SoftLayer/CLI/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 3aa5260e1..b3e33cf87 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -45,6 +45,8 @@ """ # :license: MIT, see LICENSE for more details. +# pylint: disable=W0703 + import logging import sys From 55ebdf186052b8778d2f0ed7ff514247a82a8a05 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Wed, 30 Jul 2014 13:46:02 -0500 Subject: [PATCH 0020/2592] Fix missing var test error --- SoftLayer/tests/CLI/modules/server_tests.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index a5d602f41..1429d9ffa 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -261,7 +261,8 @@ def test_list_servers(self): 'cores': 2, 'id': 1000, 'backend_ip': '10.1.0.2', - 'active_transaction': 'TXN_NAME' + 'active_transaction': 'TXN_NAME', + 'owner': 'chechu' }, { 'datacenter': 'TEST00', @@ -271,7 +272,8 @@ def test_list_servers(self): 'cores': 4, 'id': 1001, 'backend_ip': '10.1.0.3', - 'active_transaction': None + 'active_transaction': None, + 'owner': 'chechu' }, { 'datacenter': 'TEST00', @@ -281,7 +283,8 @@ def test_list_servers(self): 'cores': 4, 'id': 1002, 'backend_ip': '10.1.0.4', - 'active_transaction': None + 'active_transaction': None, + 'owner': 'chechu' } ] From 475bd30562e904ba222fc6661eba7be78c215ea8 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Wed, 30 Jul 2014 14:26:19 -0500 Subject: [PATCH 0021/2592] Fix missing price test error --- SoftLayer/tests/CLI/modules/vs_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index f09561c6c..0acd3f4a2 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -41,7 +41,6 @@ def test_list_vs(self): def test_detail_vs(self): command = vs.VSDetails(client=self.client) - output = command.execute({'': '100', '--passwords': True, '--price': True}) @@ -56,8 +55,8 @@ def test_detail_vs(self): 'modified': {}, 'os': '12.04-64 Minimal for CCI', 'os_version': '12.04-64 Minimal for CCI', - 'price rate': {}, 'notes': 'notes', + 'price rate': 1.54, 'tags': ['production'], 'private_cpu': {}, 'private_ip': '10.45.19.37', From 683e4192b561d6d7bdc58a1614e90dfe6f29abab Mon Sep 17 00:00:00 2001 From: underscorephil Date: Wed, 30 Jul 2014 14:29:33 -0500 Subject: [PATCH 0022/2592] pep8 fixes --- SoftLayer/CLI/modules/vs.py | 20 +++++++++++++++++++- SoftLayer/tests/CLI/modules/vs_tests.py | 7 ++++--- SoftLayer/tests/fixtures/Account.py | 10 +++++----- SoftLayer/tests/fixtures/Hardware_Server.py | 2 +- SoftLayer/tests/fixtures/Virtual_Guest.py | 2 +- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 365be8c48..4a0e86ede 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -43,6 +43,7 @@ class ListVSIs(CLIRunnable): + """ usage: sl vs list [--hourly | --monthly] [--sortby=SORT_COLUMN] [--tags=TAGS] [options] @@ -118,6 +119,7 @@ def execute(self, args): class VSDetails(CLIRunnable): + """ usage: sl vs detail [--passwords] [--price] [options] @@ -173,7 +175,8 @@ def execute(self, args): table.add_row(['created', result['createDate']]) table.add_row(['modified', result['modifyDate']]) table.add_row(['owner', FormattedItem( - lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + lookup(result, 'billingItem', 'orderItem', + 'order', 'userRecord', 'username'), )]) vlan_table = Table(['type', 'number', 'id']) @@ -215,6 +218,7 @@ def execute(self, args): class CreateOptionsVS(CLIRunnable): + """ usage: sl vs create-options [options] @@ -338,6 +342,7 @@ def add_block_rows(disks, name): class CreateVS(CLIRunnable): + """ usage: sl vs create [--disk=SIZE...] [--key=KEY...] [options] @@ -624,6 +629,7 @@ def _parse_create_args(self, args): class ReadyVS(CLIRunnable): + """ usage: sl vs ready [options] @@ -648,6 +654,7 @@ def execute(self, args): class ReloadVS(CLIRunnable): + """ usage: sl vs reload [--key=KEY...] [options] @@ -679,6 +686,7 @@ def execute(self, args): class CancelVS(CLIRunnable): + """ usage: sl vs cancel [options] @@ -698,6 +706,7 @@ def execute(self, args): class VSPowerOff(CLIRunnable): + """ usage: sl vs power-off [--hard] [options] @@ -724,6 +733,7 @@ def execute(self, args): class VSReboot(CLIRunnable): + """ usage: sl vs reboot [--hard | --soft] [options] @@ -753,6 +763,7 @@ def execute(self, args): class VSPowerOn(CLIRunnable): + """ usage: sl vs power-on [options] @@ -768,6 +779,7 @@ def execute(self, args): class VSPause(CLIRunnable): + """ usage: sl vs pause [options] @@ -789,6 +801,7 @@ def execute(self, args): class VSResume(CLIRunnable): + """ usage: sl vs resume [options] @@ -804,6 +817,7 @@ def execute(self, args): class NicEditVS(CLIRunnable): + """ usage: sl vs nic-edit (public | private) --speed=SPEED [options] @@ -825,6 +839,7 @@ def execute(self, args): class VSDNS(CLIRunnable): + """ usage: sl vs dns sync [options] @@ -927,6 +942,7 @@ def sync_ptr_record(): class EditVS(CLIRunnable): + """ usage: sl vs edit [options] @@ -968,6 +984,7 @@ def execute(self, args): class CaptureVS(CLIRunnable): + """ usage: sl vs capture [options] @@ -1011,6 +1028,7 @@ def execute(self, args): class UpgradeVS(CLIRunnable): + """ usage: sl vs upgrade [options] diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 0acd3f4a2..e7c7ffdfc 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -12,6 +12,7 @@ class DnsTests(TestCase): + def set_up(self): self.client = FixtureClient() @@ -27,7 +28,7 @@ def test_list_vs(self): 'active_transaction': None, 'id': 100, 'backend_ip': '10.45.19.37', - 'owner': 'chechu' }, + 'owner': 'chechu'}, {'datacenter': 'TEST00', 'primary_ip': '172.16.240.7', 'host': 'vs-test2.test.sftlyr.ws', @@ -36,7 +37,7 @@ def test_list_vs(self): 'active_transaction': None, 'id': 104, 'backend_ip': '10.45.19.35', - 'owner': 'chechu' }], + 'owner': 'chechu'}], format_output(output, 'python')) def test_detail_vs(self): @@ -56,7 +57,7 @@ def test_detail_vs(self): 'os': '12.04-64 Minimal for CCI', 'os_version': '12.04-64 Minimal for CCI', 'notes': 'notes', - 'price rate': 1.54, + 'price rate': 1.54, 'tags': ['production'], 'private_cpu': {}, 'private_ip': '10.45.19.37', diff --git a/SoftLayer/tests/fixtures/Account.py b/SoftLayer/tests/fixtures/Account.py index 1de74d1bf..99bd6ca8e 100644 --- a/SoftLayer/tests/fixtures/Account.py +++ b/SoftLayer/tests/fixtures/Account.py @@ -42,7 +42,7 @@ } } } - }, + }, }, { 'id': 104, 'hostname': 'vs-test2', @@ -67,7 +67,7 @@ } } } - }, + }, }] getMonthlyVirtualGuests = [vs for vs in getVirtualGuests @@ -88,7 +88,7 @@ } } } - }, + }, 'primaryIpAddress': '172.16.1.100', 'hostname': 'hardware-test1', 'domain': 'test.sftlyr.ws', @@ -145,7 +145,7 @@ } } } - }, + }, 'primaryIpAddress': '172.16.4.94', 'hostname': 'hardware-test2', 'domain': 'test.sftlyr.ws', @@ -187,7 +187,7 @@ } } } - }, + }, 'primaryIpAddress': '172.16.4.95', 'hostname': 'hardware-bad-memory', 'domain': 'test.sftlyr.ws', diff --git a/SoftLayer/tests/fixtures/Hardware_Server.py b/SoftLayer/tests/fixtures/Hardware_Server.py index 71e4626e6..c1ce0be0d 100644 --- a/SoftLayer/tests/fixtures/Hardware_Server.py +++ b/SoftLayer/tests/fixtures/Hardware_Server.py @@ -10,7 +10,7 @@ } } } - }, + }, 'primaryIpAddress': '172.16.1.100', 'hostname': 'hardware-test1', 'domain': 'test.sftlyr.ws', diff --git a/SoftLayer/tests/fixtures/Virtual_Guest.py b/SoftLayer/tests/fixtures/Virtual_Guest.py index be74621bc..8e5c6af10 100644 --- a/SoftLayer/tests/fixtures/Virtual_Guest.py +++ b/SoftLayer/tests/fixtures/Virtual_Guest.py @@ -12,7 +12,7 @@ } } } - }, + }, 'datacenter': {'id': 50, 'name': 'TEST00', 'description': 'Test Data Center'}, 'powerState': {'keyName': 'RUNNING', 'name': 'Running'}, From c1ff0ec9fe079b8afeea0621da679735204529bf Mon Sep 17 00:00:00 2001 From: underscorephil Date: Mon, 4 Aug 2014 10:34:21 -0500 Subject: [PATCH 0023/2592] Fix missing lib prefixes --- SoftLayer/CLI/modules/server.py | 16 ++++++++-------- SoftLayer/CLI/modules/vs.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index e6f1dcee8..7e43ef0f3 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -95,7 +95,7 @@ def execute(self, args): table.sortby = args.get('--sortby') or 'host' for server in servers: - server = NestedDict(server) + server = utils.NestedDict(server) user = None if 'billingItem' in server: if 'orderItem' in server['billingItem']: @@ -106,11 +106,11 @@ def execute(self, args): server['datacenter']['name'] or formatting.blank(), server['fullyQualifiedDomainName'], server['processorPhysicalCoreAmount'], - gb(server['memoryCapacity'] or 0), - server['primaryIpAddress'] or blank(), - server['primaryBackendIpAddress'] or blank(), - active_txn(server), - user or blank(), + formatting.gb(server['memoryCapacity'] or 0), + server['primaryIpAddress'] or formatting.blank(), + server['primaryBackendIpAddress'] or formatting.blank(), + formatting.active_txn(server), + user or formatting.blank(), ]) return table @@ -166,14 +166,14 @@ def execute(self, args): ['softwareDescription']['name'] or formatting.blank() )]) - table.add_row(['created', result['provisionDate'] or blank()]) + table.add_row(['created', result['provisionDate'] or formatting.blank()]) user = None if 'billingItem' in result: if 'orderItem' in result['billingItem']: user = (result['billingItem']['orderItem']['order'] ['userRecord']['username']) table.add_row(['owner', - user or blank()]) + user or formatting.blank()]) vlan_table = Table(['type', 'number', 'id']) for vlan in result['networkVlans']: diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 95edf794f..e7cae07a1 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -105,10 +105,10 @@ def execute(self, args): guest['datacenter']['name'] or formatting.blank(), guest['fullyQualifiedDomainName'], guest['maxCpu'], - mb_to_gb(guest['maxMemory']), - guest['primaryIpAddress'] or blank(), - guest['primaryBackendIpAddress'] or blank(), - active_txn(guest), + formatting.mb_to_gb(guest['maxMemory']), + guest['primaryIpAddress'] or formatting.blank(), + guest['primaryBackendIpAddress'] or formatting.blank(), + formatting.active_txn(guest), guest['billingItem']['orderItem']['order'] ['userRecord']['username'] ]) From 3e1934cf505bb82016427d354197b6e954835b0b Mon Sep 17 00:00:00 2001 From: underscorephil Date: Mon, 4 Aug 2014 10:39:09 -0500 Subject: [PATCH 0024/2592] Add blank return for missing owners on VSI --- SoftLayer/CLI/modules/vs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index e7cae07a1..8d7f62dc8 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -110,7 +110,7 @@ def execute(self, args): guest['primaryBackendIpAddress'] or formatting.blank(), formatting.active_txn(guest), guest['billingItem']['orderItem']['order'] - ['userRecord']['username'] + ['userRecord']['username'] or formatting.blank(), ]) return table From e9529e44f416fa2686746d1b90c3b66f51091706 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Wed, 6 Aug 2014 15:36:35 -0500 Subject: [PATCH 0025/2592] Add vs rescue option. Fix various pep8 issues. Fix missing formatting prefixs --- SoftLayer/CLI/modules/server.py | 5 +- SoftLayer/CLI/modules/vs.py | 32 +++++- SoftLayer/managers/vs.py | 9 ++ SoftLayer/testing/fixtures/Account.py | 99 ++++++++++--------- SoftLayer/testing/fixtures/Hardware_Server.py | 20 ++-- SoftLayer/testing/fixtures/Virtual_Guest.py | 21 ++-- SoftLayer/tests/CLI/modules/vs_tests.py | 4 +- SoftLayer/tests/managers/vs_tests.py | 9 ++ 8 files changed, 128 insertions(+), 71 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 7e43ef0f3..f313d5fe5 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -166,7 +166,8 @@ def execute(self, args): ['softwareDescription']['name'] or formatting.blank() )]) - table.add_row(['created', result['provisionDate'] or formatting.blank()]) + table.add_row( + ['created', result['provisionDate'] or formatting.blank()]) user = None if 'billingItem' in result: if 'orderItem' in result['billingItem']: @@ -174,7 +175,7 @@ def execute(self, args): ['userRecord']['username']) table.add_row(['owner', user or formatting.blank()]) - vlan_table = Table(['type', 'number', 'id']) + vlan_table = formatting.Table(['type', 'number', 'id']) for vlan in result['networkVlans']: vlan_table.add_row([ diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 8d7f62dc8..3135d488b 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -177,9 +177,9 @@ def execute(self, args): table.add_row(['private_cpu', result['dedicatedAccountHostOnlyFlag']]) table.add_row(['created', result['createDate']]) table.add_row(['modified', result['modifyDate']]) - table.add_row(['owner', FormattedItem( - lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', 'username'), + table.add_row(['owner', formatting.FormattedItem( + utils.lookup(result, 'billingItem', 'orderItem', + 'order', 'userRecord', 'username'), )]) vlan_table = formatting.Table(['type', 'number', 'id']) @@ -881,6 +881,32 @@ def execute(self, args): vsi.change_port_speed(vs_id, public, args['--speed']) +class VSRescue(environment.CLIRunnable): + + """ +usage: sl vs rescue [options] + +Reboot into Xen rescue image + + +""" + action = 'rescue' + options = ['confirm'] + + def execute(self, args): + vsi = SoftLayer.VSManager(self.client) + vs_id = helpers.resolve_id(vsi.resolve_ids, + args.get(''), + 'VS') + if args['--really'] or formatting.confirm( + "This action will reboot this VSI. " + "Continue?"): + + vsi.rescue(vs_id) + else: + raise exceptions.CLIAbort('Aborted') + + class VSDNS(environment.CLIRunnable): """ diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 5c12b6f15..55c718a5e 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -15,6 +15,7 @@ class VSManager(utils.IdentifierMixin, object): + """ Manages Virtual Servers @@ -24,6 +25,7 @@ class VSManager(utils.IdentifierMixin, object): If none is provided, one will be auto initialized. """ + def __init__(self, client, ordering_manager=None): self.client = client self.account = client['Account'] @@ -556,6 +558,13 @@ def edit(self, instance_id, userdata=None, hostname=None, domain=None, return self.guest.editObject(obj, id=instance_id) + def rescue(self, instance_id): + """ Reboot a VSI into the Xen recsue kernel + + :param integer instance_id: the instance ID to rescue + """ + return self.guest.executeRescueLayer(id=instance_id) + def capture(self, instance_id, name, additional_disks=False, notes=None): """ Capture one or all disks from a VS to a SoftLayer image. diff --git a/SoftLayer/testing/fixtures/Account.py b/SoftLayer/testing/fixtures/Account.py index 3778e4b25..6cf237a5b 100644 --- a/SoftLayer/testing/fixtures/Account.py +++ b/SoftLayer/testing/fixtures/Account.py @@ -1,4 +1,3 @@ - getPrivateBlockDeviceTemplateGroups = [{ 'accountId': 1234, 'blockDevices': [], @@ -33,15 +32,17 @@ 'primaryBackendIpAddress': '10.45.19.37', 'hourlyBillingFlag': False, - 'billingItem': {'id': 6327, 'recurringFee': 1.54, - 'orderItem': { - 'order': { - 'userRecord': { - 'username': 'chechu', - } - } - } - }, + 'billingItem': { + 'id': 6327, + 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, }, { 'id': 104, 'hostname': 'vs-test2', @@ -57,15 +58,17 @@ 'globalIdentifier': '05a8ac-6abf0', 'primaryBackendIpAddress': '10.45.19.35', 'hourlyBillingFlag': True, - 'billingItem': {'id': 6327, 'recurringFee': 1.54, - 'orderItem': { - 'order': { - 'userRecord': { - 'username': 'chechu', - } - } - } - }, + 'billingItem': { + 'id': 6327, + 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, }] getMonthlyVirtualGuests = [vs for vs in getVirtualGuests @@ -78,15 +81,17 @@ 'id': 1000, 'datacenter': {'id': 50, 'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 6327, 'recurringFee': 1.54, - 'orderItem': { - 'order': { - 'userRecord': { - 'username': 'chechu', - } - } - } - }, + 'billingItem': { + 'id': 6327, + 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.1.100', 'hostname': 'hardware-test1', 'domain': 'test.sftlyr.ws', @@ -135,15 +140,16 @@ 'id': 1001, 'datacenter': {'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 7112, - 'orderItem': { - 'order': { - 'userRecord': { - 'username': 'chechu', - } - } - } - }, + 'billingItem': { + 'id': 7112, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.4.94', 'hostname': 'hardware-test2', 'domain': 'test.sftlyr.ws', @@ -177,15 +183,16 @@ 'id': 1002, 'datacenter': {'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 7112, - 'orderItem': { - 'order': { - 'userRecord': { - 'username': 'chechu', - } - } - } - }, + 'billingItem': { + 'id': 7112, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.4.95', 'hostname': 'hardware-bad-memory', 'domain': 'test.sftlyr.ws', diff --git a/SoftLayer/testing/fixtures/Hardware_Server.py b/SoftLayer/testing/fixtures/Hardware_Server.py index c1ce0be0d..883c7741c 100644 --- a/SoftLayer/testing/fixtures/Hardware_Server.py +++ b/SoftLayer/testing/fixtures/Hardware_Server.py @@ -2,15 +2,17 @@ 'id': 1000, 'datacenter': {'id': 50, 'name': 'TEST00', 'description': 'Test Data Center'}, - 'billingItem': {'id': 6327, 'recurringFee': 1.54, - 'orderItem': { - 'order': { - 'userRecord': { - 'username': 'chechu', - } - } - } - }, + 'billingItem': { + 'id': 6327, + 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'primaryIpAddress': '172.16.1.100', 'hostname': 'hardware-test1', 'domain': 'test.sftlyr.ws', diff --git a/SoftLayer/testing/fixtures/Virtual_Guest.py b/SoftLayer/testing/fixtures/Virtual_Guest.py index 8641aa002..de9f16ca2 100644 --- a/SoftLayer/testing/fixtures/Virtual_Guest.py +++ b/SoftLayer/testing/fixtures/Virtual_Guest.py @@ -4,15 +4,17 @@ 'domain': 'test.sftlyr.ws', 'fullyQualifiedDomainName': 'vs-test1.test.sftlyr.ws', 'status': {'keyName': 'ACTIVE', 'name': 'Active'}, - 'billingItem': {'id': 6327, 'recurringFee': 1.54, - 'orderItem': { - 'order': { - 'userRecord': { - 'username': 'chechu', - } - } - } - }, + 'billingItem': { + 'id': 6327, + 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, 'datacenter': {'id': 50, 'name': 'TEST00', 'description': 'Test Data Center'}, 'powerState': {'keyName': 'RUNNING', 'name': 'Running'}, @@ -233,3 +235,4 @@ reloadOperatingSystem = 'OK' setTags = True createArchiveTransaction = {} +executeRescueLayer = True diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index d27a6ab7e..e9b945768 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -38,7 +38,7 @@ def test_list_vs(self): 'id': 104, 'backend_ip': '10.45.19.35', 'owner': 'chechu'}], - format_output(output, 'python')) + formatting.format_output(output, 'python')) def test_detail_vs(self): command = vs.VSDetails(client=self.client) @@ -71,7 +71,7 @@ def test_detail_vs(self): 'number': 23, 'id': 1}], 'owner': 'chechu'}, - format_output(output, 'python')) + formatting.format_output(output, 'python')) def test_create_options(self): command = vs.CreateOptionsVS(client=self.client) diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index a0e7bcea3..bd2bd2f04 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -516,6 +516,15 @@ def test_change_port_speed_private(self): f = service.setPrivateNetworkInterfaceSpeed f.assert_called_once_with(speed, id=vs_id) + def test_rescue(self): + # Test rescue environment + vs_id = 1234 + self.vs.rescue(vs_id) + + service = self.client['Virtual_Guest'] + f = service.executeRescueLayer + f.assert_called_once_with(id=vs_id) + def test_edit(self): # Test editing user data service = self.client['Virtual_Guest'] From a19fa5809016dfd441315692441122c36e7c5229 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Thu, 7 Aug 2014 13:44:45 -0500 Subject: [PATCH 0026/2592] Revert pep8 changes and use utils.lookup --- SoftLayer/CLI/modules/server.py | 38 ++++++++++----------------------- SoftLayer/CLI/modules/vs.py | 24 ++++----------------- 2 files changed, 15 insertions(+), 47 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index f313d5fe5..250a378cd 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -36,7 +36,6 @@ class ListServers(environment.CLIRunnable): - """ usage: sl server list [options] @@ -96,11 +95,7 @@ def execute(self, args): for server in servers: server = utils.NestedDict(server) - user = None - if 'billingItem' in server: - if 'orderItem' in server['billingItem']: - user = (server['billingItem']['orderItem']['order'] - ['userRecord']['username']) + table.add_row([ server['id'], server['datacenter']['name'] or formatting.blank(), @@ -110,14 +105,15 @@ def execute(self, args): server['primaryIpAddress'] or formatting.blank(), server['primaryBackendIpAddress'] or formatting.blank(), formatting.active_txn(server), - user or formatting.blank(), + utils.lookup( + server, 'billingItem', 'orderItem', 'order', 'userRecord', + 'username') or formatting.blank(), ]) return table class ServerDetails(environment.CLIRunnable): - """ usage: sl server detail [--passwords] [--price] [options] @@ -168,13 +164,13 @@ def execute(self, args): table.add_row( ['created', result['provisionDate'] or formatting.blank()]) - user = None - if 'billingItem' in result: - if 'orderItem' in result['billingItem']: - user = (result['billingItem']['orderItem']['order'] - ['userRecord']['username']) - table.add_row(['owner', - user or formatting.blank()]) + + table.add_row(['owner', formatting.FormattedItem( + utils.lookup(result, 'billingItem', 'orderItem', + 'order', 'userRecord', 'username') \ + or formatting.blank(), + )]) + vlan_table = formatting.Table(['type', 'number', 'id']) for vlan in result['networkVlans']: @@ -216,7 +212,6 @@ def execute(self, args): class ServerReload(environment.CLIRunnable): - """ usage: sl server reload [--key=KEY...] [options] @@ -249,7 +244,6 @@ def execute(self, args): class CancelServer(environment.CLIRunnable): - """ usage: sl server cancel [options] @@ -283,7 +277,6 @@ def execute(self, args): class ServerCancelReasons(environment.CLIRunnable): - """ usage: sl server cancel-reasons @@ -306,7 +299,6 @@ def execute(self, args): class ServerPowerOff(environment.CLIRunnable): - """ usage: sl server power-off [options] @@ -329,7 +321,6 @@ def execute(self, args): class ServerReboot(environment.CLIRunnable): - """ usage: sl server reboot [--hard | --soft] [options] @@ -362,7 +353,6 @@ def execute(self, args): class ServerPowerOn(environment.CLIRunnable): - """ usage: sl server power-on [options] @@ -379,7 +369,6 @@ def execute(self, args): class ServerPowerCycle(environment.CLIRunnable): - """ usage: sl server power-cycle [options] @@ -403,7 +392,6 @@ def execute(self, args): class NicEditServer(environment.CLIRunnable): - """ usage: sl server nic-edit (public | private) --speed=SPEED [options] @@ -428,7 +416,6 @@ def execute(self, args): class ListChassisServer(environment.CLIRunnable): - """ usage: sl server list-chassis [options] @@ -451,7 +438,6 @@ def execute(self, args): class ServerCreateOptions(environment.CLIRunnable): - """ usage: sl server create-options [options] @@ -784,7 +770,6 @@ def _generate_windows_code(self, description): class CreateServer(environment.CLIRunnable): - """ usage: sl server create [--disk=SIZE...] [--key=KEY...] [options] @@ -1049,7 +1034,6 @@ def _get_price_id_from_options(self, ds_options, option, value, class EditServer(environment.CLIRunnable): - """ usage: sl server edit [options] diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 3135d488b..086818cd1 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -41,7 +41,6 @@ class ListVSIs(environment.CLIRunnable): - """ usage: sl vs list [--hourly | --monthly] [--sortby=SORT_COLUMN] [--tags=TAGS] [options] @@ -109,15 +108,14 @@ def execute(self, args): guest['primaryIpAddress'] or formatting.blank(), guest['primaryBackendIpAddress'] or formatting.blank(), formatting.active_txn(guest), - guest['billingItem']['orderItem']['order'] - ['userRecord']['username'] or formatting.blank(), + utils.lookup(guest, 'billingItem', 'orderItem', 'order', + 'userRecord', 'username') or formatting.blank(), ]) return table class VSDetails(environment.CLIRunnable): - """ usage: sl vs detail [--passwords] [--price] [options] @@ -179,7 +177,8 @@ def execute(self, args): table.add_row(['modified', result['modifyDate']]) table.add_row(['owner', formatting.FormattedItem( utils.lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', 'username'), + 'order', 'userRecord', 'username') \ + or formatting.blank(), )]) vlan_table = formatting.Table(['type', 'number', 'id']) @@ -221,7 +220,6 @@ def execute(self, args): class CreateOptionsVS(environment.CLIRunnable): - """ usage: sl vs create-options [options] @@ -348,7 +346,6 @@ def add_block_rows(disks, name): class CreateVS(environment.CLIRunnable): - """ usage: sl vs create [--disk=SIZE...] [--key=KEY...] [options] @@ -678,7 +675,6 @@ def execute(self, args): class ReloadVS(environment.CLIRunnable): - """ usage: sl vs reload [--key=KEY...] [options] @@ -712,7 +708,6 @@ def execute(self, args): class CancelVS(environment.CLIRunnable): - """ usage: sl vs cancel [options] @@ -734,7 +729,6 @@ def execute(self, args): class VSPowerOff(environment.CLIRunnable): - """ usage: sl vs power-off [--hard] [options] @@ -764,7 +758,6 @@ def execute(self, args): class VSReboot(environment.CLIRunnable): - """ usage: sl vs reboot [--hard | --soft] [options] @@ -797,7 +790,6 @@ def execute(self, args): class VSPowerOn(environment.CLIRunnable): - """ usage: sl vs power-on [options] @@ -815,7 +807,6 @@ def execute(self, args): class VSPause(environment.CLIRunnable): - """ usage: sl vs pause [options] @@ -840,7 +831,6 @@ def execute(self, args): class VSResume(environment.CLIRunnable): - """ usage: sl vs resume [options] @@ -858,7 +848,6 @@ def execute(self, args): class NicEditVS(environment.CLIRunnable): - """ usage: sl vs nic-edit (public | private) --speed=SPEED [options] @@ -882,7 +871,6 @@ def execute(self, args): class VSRescue(environment.CLIRunnable): - """ usage: sl vs rescue [options] @@ -908,7 +896,6 @@ def execute(self, args): class VSDNS(environment.CLIRunnable): - """ usage: sl vs dns sync [options] @@ -1017,7 +1004,6 @@ def sync_ptr_record(): class EditVS(environment.CLIRunnable): - """ usage: sl vs edit [options] @@ -1063,7 +1049,6 @@ def execute(self, args): class CaptureVS(environment.CLIRunnable): - """ usage: sl vs capture [options] @@ -1109,7 +1094,6 @@ def execute(self, args): class UpgradeVS(environment.CLIRunnable): - """ usage: sl vs upgrade [options] From 855c8f683f14b1d4fc1f02affc6cf766090896bf Mon Sep 17 00:00:00 2001 From: underscorephil Date: Thu, 7 Aug 2014 13:52:00 -0500 Subject: [PATCH 0027/2592] pep8 fixes --- SoftLayer/CLI/modules/server.py | 6 +++--- SoftLayer/CLI/modules/vs.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 250a378cd..66417b7b9 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -95,7 +95,7 @@ def execute(self, args): for server in servers: server = utils.NestedDict(server) - + table.add_row([ server['id'], server['datacenter']['name'] or formatting.blank(), @@ -167,8 +167,8 @@ def execute(self, args): table.add_row(['owner', formatting.FormattedItem( utils.lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', 'username') \ - or formatting.blank(), + 'order', 'userRecord', + 'username') or formatting.blank() )]) vlan_table = formatting.Table(['type', 'number', 'id']) diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 086818cd1..6f5400855 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -177,8 +177,8 @@ def execute(self, args): table.add_row(['modified', result['modifyDate']]) table.add_row(['owner', formatting.FormattedItem( utils.lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', 'username') \ - or formatting.blank(), + 'order', 'userRecord', + 'username') or formatting.blank(), )]) vlan_table = formatting.Table(['type', 'number', 'id']) From bc8a6e9afa8eb5480fd3ad445ea723d27b335a65 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Thu, 7 Aug 2014 17:44:37 -0500 Subject: [PATCH 0028/2592] Add basic quote manager and tests --- SoftLayer/managers/ordering.py | 75 ++++++++++++++++++++++ SoftLayer/testing/fixtures/Account.py | 6 ++ SoftLayer/tests/managers/ordering_tests.py | 30 +++++++++ 3 files changed, 111 insertions(+) diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 649015601..3942e7df3 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -116,3 +116,78 @@ def get_package_id_by_type(self, package_type): return package['id'] else: raise ValueError("No package found for type: " + package_type) + + def get_quotes(self): + """ Retrieve a list of quotes + :return a list of SoftLayer_Billing_Order_Quote + """ + quotes = self.client['Account'].getActiveQuotes() + return quotes + + def get_quote_details(self, quote_id): + """ Retrieve quote details + :param quote_id ID number of target quote + """ + quote = self.client['Billing_Order_Quote'].getObject(id=quote_id) + return quote + + def get_order_container(self, quote_id): + """ Generate an order container from a quote object + :param quote_id ID number of target quote + """ + quote = self.client['Billing_Order_Quote'] + container = quote.getRecalculatedOrderContainer(id=quote_id) + return container['orderContainers'][0] + + def generate_order_template(self, quote_id=None, hostnames=None, + domain=None, quantity=None): + """ Generate a complete order template + :param int quote_id: ID of target quote + :param list hostnames: List of hostnames as strings + :param string domain: Domain name to be used for all servers + :param int quantity: Quantity to override default + """ + container = self.get_order_container(quote_id) + if quantity is not None: + container['quantity'] = quantity + if container['packageId'] == 46: + product_type = 'virtualGuests' + else: + product_type = 'hardware' + + if len(hostnames) != container['quantity']: + raise ValueError("You must specify a hostname for each " + "server in the quote") + + container[product_type] = [] + for hostname in hostnames: + container[product_type].append( + {'hostname': hostname, 'domain': domain} + ) + container['presetId'] = None + return container + + def verify_quote(self, **kwargs): + """ + Verifies that a quote order is valid without actually ordering + the resources + + :param int quote_id: ID for the target quote + :param list hostnames: hostnames of the servers + :param string domain: domain of the new servers + :param int quantity: Quantity to override default + """ + container = self.generate_order_template(**kwargs) + return self.client['Product_Order'].verifyOrder(container) + + def order_quote(self, **kwargs): + """ + Places an order using a quote + + :param int quote_id: ID for the target quote + :param list hostnames: hostnames of the servers + :param string domain: domain of the new server + :param int quantity: Quantity to override default + """ + container = self.generate_order_template(**kwargs) + return self.client['Product_Order'].placeOrder(container) diff --git a/SoftLayer/testing/fixtures/Account.py b/SoftLayer/testing/fixtures/Account.py index 6cf237a5b..b3f0b64f0 100644 --- a/SoftLayer/testing/fixtures/Account.py +++ b/SoftLayer/testing/fixtures/Account.py @@ -418,3 +418,9 @@ 'password': 'pass', 'serviceResourceBackendIpAddress': '127.0.0.1', }] + +getActiveQuotes = [{ + 'id': 1234, + 'name': 'TestQuote1234', + 'quoteKey': '1234test4321', +}] diff --git a/SoftLayer/tests/managers/ordering_tests.py b/SoftLayer/tests/managers/ordering_tests.py index d723792db..66ffcda86 100644 --- a/SoftLayer/tests/managers/ordering_tests.py +++ b/SoftLayer/tests/managers/ordering_tests.py @@ -69,3 +69,33 @@ def test_get_package_id_by_type_fails_for_nonexistent_package_type(self): self.ordering.client['Product_Package'].getAllObjects.return_value = [] with self.assertRaises(ValueError): self.ordering.get_package_id_by_type(package_type) + + def test_get_order_container(self): + container = self.ordering.get_order_container(1234) + quote = self.ordering.client['Billing_Order_Quote'] + container_fixture = quote.getRecalculatedOrderContainer(id=1234) + self.assertEqual(container, container_fixture['orderContainers'][0]) + + def test_get_quotes(self): + quotes = self.ordering.get_quotes() + quotes_fixture = self.ordering.client['Account'].getActiveQuotes() + self.assertEqual(quotes, quotes_fixture) + + def test_get_quote_details(self): + quote = self.ordering.get_quote_details(1234) + quote_fixture = self.ordering.client['Billing_Order_Quote'].getObject( + id=1234) + self.assertEqual(quote, quote_fixture) + + def test_verify_quote(self): + result = self.ordering.verify_quote( + quote_id=1234, + domain='example.com', + hostnames=['test1'], + quantity=1) + + self.assertEqual(result, self.ordering.client['Product_Order']. + verifyOrder()) + + def test_order_quote(self): + return True From e0d075661677b4b02fa29d108472e80b9fbcad02 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Thu, 7 Aug 2014 17:48:20 -0500 Subject: [PATCH 0029/2592] Add quote fixture --- .../testing/fixtures/Billing_Order_Quote.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 SoftLayer/testing/fixtures/Billing_Order_Quote.py diff --git a/SoftLayer/testing/fixtures/Billing_Order_Quote.py b/SoftLayer/testing/fixtures/Billing_Order_Quote.py new file mode 100644 index 000000000..6302bfa94 --- /dev/null +++ b/SoftLayer/testing/fixtures/Billing_Order_Quote.py @@ -0,0 +1,18 @@ +getObject = { + 'accountId': 1234, + 'id': 1234, + 'name': 'TestQuote1234', + 'quoteKey': '1234test4321', +} + +getRecalculatedOrderContainer = { + 'orderContainers': [{ + 'presetId': '', + 'prices': [{ + 'id': 1921 + }], + 'quantity': 1, + 'packageId': 50, + 'useHourlyPricing': '', + }], +} From 8e62dd6bc964244865356f44fe827ff49ac50b2b Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Thu, 7 Aug 2014 23:29:45 -0500 Subject: [PATCH 0030/2592] removing = from argument values --- .idea/scopes/scope_settings.xml | 5 + .idea/workspace.xml | 397 ++++++++++++++++++++++++++++++++ SoftLayer/CLI/modules/vs.py | 1 + SoftLayer/utils.py | 7 + 4 files changed, 410 insertions(+) create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/workspace.xml diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 000000000..922003b84 --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000..0a852168e --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,397 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1407442328001 + 1407442328001 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 6f5400855..e331c83ec 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -577,6 +577,7 @@ def _parse_create_args(self, args): :param dict args: CLI arguments """ + args = utils.sanitize_args(args) data = { "hourly": args['--hourly'], "cpus": args['--cpu'], diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 0162dc868..ad49b7688 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -173,3 +173,10 @@ def dict_extract(dictionary, keys): if dictionary.get(key, None) is not None: del dictionary[key] return tuple(vals) + + +def sanitize_args(args): + for key, value in args.items(): + if isinstance(value, str) and value.startswith('='): + args[key] = value[1:] + return args \ No newline at end of file From 17ab2c171bb3714a2ed3af7bd0a622389666475c Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Thu, 7 Aug 2014 23:33:18 -0500 Subject: [PATCH 0031/2592] removing unintended changes --- .idea/scopes/scope_settings.xml | 5 - .idea/workspace.xml | 397 -------------------------------- 2 files changed, 402 deletions(-) delete mode 100644 .idea/scopes/scope_settings.xml delete mode 100644 .idea/workspace.xml diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml deleted file mode 100644 index 922003b84..000000000 --- a/.idea/scopes/scope_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 0a852168e..000000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,397 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1407442328001 - 1407442328001 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From fabdfc93a09ef12aff5c00fcd6f14de07675ad68 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Thu, 7 Aug 2014 23:35:31 -0500 Subject: [PATCH 0032/2592] add new line --- SoftLayer/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index ad49b7688..57b44629f 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -179,4 +179,4 @@ def sanitize_args(args): for key, value in args.items(): if isinstance(value, str) and value.startswith('='): args[key] = value[1:] - return args \ No newline at end of file + return args From cc85ca57618febb6d33c76ddaf6057f024ec3b66 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Fri, 8 Aug 2014 09:54:50 -0500 Subject: [PATCH 0033/2592] adding doctring --- SoftLayer/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 57b44629f..c0cce001a 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -176,6 +176,9 @@ def dict_extract(dictionary, keys): def sanitize_args(args): + """ sanitize input (remove = sign from argument values) + :returns args back + """ for key, value in args.items(): if isinstance(value, str) and value.startswith('='): args[key] = value[1:] From 9199ac202f2eb8981be0d5a842e514bac5be8f61 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 8 Aug 2014 10:21:37 -0500 Subject: [PATCH 0034/2592] Removes dict_extract(). Renames 'tag' argument to 'tags' *** This will be a breaking change for people working off of master*** --- SoftLayer/managers/vs.py | 9 ++++----- SoftLayer/tests/CLI/modules/vs_tests.py | 2 +- SoftLayer/tests/managers/vs_tests.py | 5 +++-- SoftLayer/utils.py | 17 ----------------- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 55c718a5e..d8538fc5b 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -458,10 +458,10 @@ def create_instance(self, **kwargs): :param int nic_speed: The port speed to set :param string tag: tags to set on the VS as a comma separated list """ - tag, = utils.dict_extract(kwargs, {'tag': None}) + tags = kwargs.pop('tags', None) inst = self.guest.createObject(self._generate_create_dict(**kwargs)) - if tag is not None: - self.guest.setTags(tag, id=inst['id']) + if tags is not None: + self.guest.setTags(tags, id=inst['id']) return inst def create_instances(self, config_list): @@ -470,8 +470,7 @@ def create_instances(self, config_list): This takes a list of dictionaries using the same arguments as create_instance(). """ - tags = [utils.dict_extract(conf, {'tag': None})[0] - for conf in config_list] + tags = [conf.pop('tags', None) for conf in config_list] resp = self.guest.createObjects([self._generate_create_dict(**kwargs) for kwargs in config_list]) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index e9b945768..bc3f6d384 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -128,7 +128,7 @@ def test_create(self, confirm_mock): '--vlan_private': None, '--wait': None, '--really': False, - '--tag': 'dev,green'}) + '--tags': 'dev,green'}) self.assertEqual([{'guid': '1a2b3c-1701', 'id': 100, diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index bd2bd2f04..4c7493587 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -137,7 +137,8 @@ def test_create_verify(self, create_dict): @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') def test_create_instance(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} - self.vs.create_instance(test=1, verify=1, tag='dev,green') + self.vs.create_instance(test=1, verify=1, tags='dev,green') + create_dict.assert_called_once_with(test=1, verify=1) self.client['Virtual_Guest'].createObject.assert_called_once_with( {'test': 1, 'verify': 1}) @@ -149,7 +150,7 @@ def test_create_instances(self): 'memory': 1024, 'hostname': 'server', 'domain': 'example.com', - 'tag': 'dev,green'}]) + 'tags': 'dev,green'}]) self.client['Virtual_Guest'].createObjects.assert_called_once_with([ {'domain': 'example.com', 'hourlyBillingFlag': True, diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 0162dc868..e0d28239d 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -156,20 +156,3 @@ def resolve_ids(identifier, resolvers): return ids return [] - - -def dict_extract(dictionary, keys): - """Extracts & removes keys from a dict with a default value. - - :param dict dictionary: the target dictionary to operate on - :param dict keys: a dict who's keys specify the keys to extract - from the dictionary and values are the defaults to use if - the key is not in the target dict. - :returns tuple - """ - vals = [] - for key, default in keys.items(): - vals.append(dictionary.get(key, default)) - if dictionary.get(key, None) is not None: - del dictionary[key] - return tuple(vals) From 002f01657c831588f246f44760a4886d2f25b38d Mon Sep 17 00:00:00 2001 From: underscorephil Date: Fri, 8 Aug 2014 13:58:40 -0500 Subject: [PATCH 0035/2592] Add hardware support for rescue env ref issues/371 --- SoftLayer/CLI/modules/server.py | 25 +++++++++++++++++++++++++ SoftLayer/managers/hardware.py | 7 +++++++ 2 files changed, 32 insertions(+) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 66417b7b9..d767aa373 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -436,6 +436,31 @@ def execute(self, args): return table +class ServerRescue(environment.CLIRunnable): + """ +usage: sl server rescue [options] + +Reboot server into a rescue image + + +""" + action = 'rescue' + options = ['confirm'] + + def execute(self, args): + server = SoftLayer.HardwareManager(self.client) + server_id = helpers.resolve_id(server.resolve_ids, + args.get(''), + 'hardware') + + if args['--really'] or formatting.confirm( + "This action will reboot this server. " + "Continue?"): + + server.rescue(server_id) + else: + raise exceptions.CLIAbort('Aborted') + class ServerCreateOptions(environment.CLIRunnable): """ diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index f96d599ef..7f6de620c 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -318,6 +318,13 @@ def reload(self, hardware_id, post_uri=None, ssh_keys=None): return self.hardware.reloadOperatingSystem('FORCE', config, id=hardware_id) + def rescue(self, hardware_id): + """ Reboot a server into the a recsue kernel + + :param integer instance_id: the server ID to rescue + """ + return self.hardware.bootToRescueLayer(id=hardware_id) + def change_port_speed(self, hardware_id, public, speed): """ Allows you to change the port speed of a server's NICs. From 8c2f58f3295de1458f39a5e22b6aea627b63626d Mon Sep 17 00:00:00 2001 From: underscorephil Date: Fri, 8 Aug 2014 14:06:06 -0500 Subject: [PATCH 0036/2592] Add tests for hardware rescue --- SoftLayer/testing/fixtures/Hardware_Server.py | 1 + SoftLayer/tests/managers/hardware_tests.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/SoftLayer/testing/fixtures/Hardware_Server.py b/SoftLayer/testing/fixtures/Hardware_Server.py index 883c7741c..98fb903f7 100644 --- a/SoftLayer/testing/fixtures/Hardware_Server.py +++ b/SoftLayer/testing/fixtures/Hardware_Server.py @@ -73,3 +73,4 @@ reloadOperatingSystem = 'OK' getReverseDomainRecords = [ {'resourceRecords': [{'data': '2.0.1.10.in-addr.arpa'}]}] +bootToRescueLayer = True diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 147eee3a0..51fd22f08 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -430,3 +430,12 @@ def test_edit(self): self.hardware.edit(100, **args) service.editObject.assert_called_once_with(args, id=100) + + def test_rescue(self): + # Test rescue environment + hardware_id = 1234 + self.hardware.rescue(hardware_id) + + service = self.client['Hardware_Server'] + f = service.bootToRescueLayer + f.assert_called_once_with(id=hardware_id) From ae2c1d13e4d10d57beec08a15ead735a62db72f0 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Fri, 8 Aug 2014 14:12:45 -0500 Subject: [PATCH 0037/2592] Fix pep8 issue --- SoftLayer/CLI/modules/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index d767aa373..5b663fe72 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -436,6 +436,7 @@ def execute(self, args): return table + class ServerRescue(environment.CLIRunnable): """ usage: sl server rescue [options] From ae0b5a440d126ccc30012ed3f7068dfde87035db Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Fri, 8 Aug 2014 14:37:14 -0500 Subject: [PATCH 0038/2592] adding testcases and moving function from utils to helpers --- SoftLayer/CLI/helpers.py | 10 ++++++++++ SoftLayer/CLI/modules/vs.py | 2 +- SoftLayer/tests/CLI/helper_tests.py | 17 ++++++++++++++++- SoftLayer/tests/CLI/modules/vs_tests.py | 2 +- SoftLayer/utils.py | 10 ---------- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index 135e36052..60d2852e9 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -30,3 +30,13 @@ def resolve_id(resolver, identifier, name='object'): (name, identifier, ', '.join([str(_id) for _id in ids]))) return ids[0] + + +def sanitize_args(args): + """ sanitize input (remove = sign from argument values) + :returns args back + """ + for key, value in args.items(): + if isinstance(value, str) and value.startswith('='): + args[key] = value[1:] + return args diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index e331c83ec..679acef60 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -577,7 +577,7 @@ def _parse_create_args(self, args): :param dict args: CLI arguments """ - args = utils.sanitize_args(args) + args = helpers.sanitize_args(args) data = { "hourly": args['--hourly'], "cpus": args['--cpu'], diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index e6dfdd2c7..3c804c688 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -249,7 +249,6 @@ class ResolveIdTests(testing.TestCase): def test_resolve_id_one(self): resolver = lambda r: [12345] id = helpers.resolve_id(resolver, 'test') - self.assertEqual(id, 12345) def test_resolve_id_none(self): @@ -423,3 +422,19 @@ def test_export_to_template(self): mock.call('datacenter=ams01\n'), mock.call('disk=disk1,disk2\n'), ], any_order=True) # Order isn't really guaranteed + + +class TestInputSanitization(testing.TestCase): + + def test_equalto_removed(self): + args = { + '--os': None, + '--datacenter': '=ams01', + '--memory': '=1g', + '--test': '=1344'} + argnew = helpers.sanitize_args(args) + self.assertEqual(argnew, { + '--os': None, + '--datacenter': 'ams01', + '--memory': '1g', + '--test': '1344'}) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index e9b945768..3599287ff 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -105,7 +105,7 @@ def test_create(self, confirm_mock): '--hostname': 'host', '--image': None, '--os': 'UBUNTU_LATEST', - '--memory': '1024', + '--memory': '=1024', '--nic': '100', '--hourly': True, '--monthly': False, diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index c0cce001a..0162dc868 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -173,13 +173,3 @@ def dict_extract(dictionary, keys): if dictionary.get(key, None) is not None: del dictionary[key] return tuple(vals) - - -def sanitize_args(args): - """ sanitize input (remove = sign from argument values) - :returns args back - """ - for key, value in args.items(): - if isinstance(value, str) and value.startswith('='): - args[key] = value[1:] - return args From a48933b2171857466baef4f3a03c1c2616d3ae0d Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 11 Aug 2014 13:42:56 -0500 Subject: [PATCH 0039/2592] check argument values --- SoftLayer/tests/CLI/modules/vs_tests.py | 78 ++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 3599287ff..ecae41308 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -105,7 +105,7 @@ def test_create(self, confirm_mock): '--hostname': 'host', '--image': None, '--os': 'UBUNTU_LATEST', - '--memory': '=1024', + '--memory': '1024', '--nic': '100', '--hourly': True, '--monthly': False, @@ -134,3 +134,79 @@ def test_create(self, confirm_mock): 'id': 100, 'created': '2013-08-01 15:23:45'}], formatting.format_output(output, 'python')) + + def test_create_args(self, confirm_mock): + confirm_mock.return_value = True + command = vs.CreateVS(client=self.client) + output = command.execute({'--cpu': '2', + '--domain': 'example.com', + '--hostname': 'host', + '--image': None, + '--os': 'UBUNTU_LATEST', + '--memory': '=1024', + '--nic': '100', + '--hourly': True, + '--monthly': False, + '--like': None, + '--datacenter': None, + '--dedicated': False, + '--san': False, + '--test': False, + '--export': None, + '--userfile': None, + '--postinstall': None, + '--key': [], + '--network': [], + '--disk': [], + '--private': False, + '--template': None, + '--userdata': None, + '--vlan_public': None, + '--vlan_private': None, + '--wait': None, + '--really': False, + '--tag': 'dev,green'}) + + self.assertEqual([{'guid': '1a2b3c-1701', + 'id': 100, + 'created': '2013-08-01 15:23:45'}], + formatting.format_output(output, 'python')) + + def test_check_create_args(self, confirm_mock): + confirm_mock.return_value = True + command = vs.CreateVS(client=self.client) + output = command.execute({'--cpu': '2', + '--domain': 'example.com', + '--hostname': 'host', + '--image': None, + '--os': 'UBUNTU_LATEST', + '--memory': '=1024', + '--nic': '100', + '--hourly': True, + '--monthly': False, + '--like': None, + '--datacenter': None, + '--dedicated': False, + '--san': False, + '--test': False, + '--export': None, + '--userfile': None, + '--postinstall': None, + '--key': [], + '--network': [], + '--disk': [], + '--private': False, + '--template': None, + '--userdata': None, + '--vlan_public': None, + '--vlan_private': None, + '--wait': None, + '--really': False, + '--tag': 'dev,green'}) + + self.assertEqual([{'guid': '1a2b3c-1701', + 'id': 100, + 'created': '2013-08-01 15:23:45'}], + formatting.format_output(output, 'python')) + service = self.client['Virtual_Guest'] + service.createObject.assert_called_with(memory=1024) \ No newline at end of file From ba9a034710fe340e533794dc59128a733c533196 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 11 Aug 2014 15:43:30 -0500 Subject: [PATCH 0040/2592] check argument values --- SoftLayer/tests/CLI/modules/vs_tests.py | 38 +------------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index ecae41308..67fad1b72 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -135,43 +135,7 @@ def test_create(self, confirm_mock): 'created': '2013-08-01 15:23:45'}], formatting.format_output(output, 'python')) - def test_create_args(self, confirm_mock): - confirm_mock.return_value = True - command = vs.CreateVS(client=self.client) - output = command.execute({'--cpu': '2', - '--domain': 'example.com', - '--hostname': 'host', - '--image': None, - '--os': 'UBUNTU_LATEST', - '--memory': '=1024', - '--nic': '100', - '--hourly': True, - '--monthly': False, - '--like': None, - '--datacenter': None, - '--dedicated': False, - '--san': False, - '--test': False, - '--export': None, - '--userfile': None, - '--postinstall': None, - '--key': [], - '--network': [], - '--disk': [], - '--private': False, - '--template': None, - '--userdata': None, - '--vlan_public': None, - '--vlan_private': None, - '--wait': None, - '--really': False, - '--tag': 'dev,green'}) - - self.assertEqual([{'guid': '1a2b3c-1701', - 'id': 100, - 'created': '2013-08-01 15:23:45'}], - formatting.format_output(output, 'python')) - + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_check_create_args(self, confirm_mock): confirm_mock.return_value = True command = vs.CreateVS(client=self.client) From ac394d829ba9c7a9c97cdb5dd196aa5fa37ccc73 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 11 Aug 2014 16:12:00 -0500 Subject: [PATCH 0041/2592] check argument values --- SoftLayer/tests/CLI/modules/vs_tests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 67fad1b72..609c5e9f4 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -165,12 +165,11 @@ def test_check_create_args(self, confirm_mock): '--vlan_public': None, '--vlan_private': None, '--wait': None, - '--really': False, - '--tag': 'dev,green'}) + '--really': False}) self.assertEqual([{'guid': '1a2b3c-1701', 'id': 100, 'created': '2013-08-01 15:23:45'}], formatting.format_output(output, 'python')) service = self.client['Virtual_Guest'] - service.createObject.assert_called_with(memory=1024) \ No newline at end of file + service.createObject.assert_called_with(maxMemory=1024, mask=mock.ANY) \ No newline at end of file From cb48379bd2a0e758ab6e9b22d4c504f93ef0e937 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Tue, 12 Aug 2014 10:11:17 -0500 Subject: [PATCH 0042/2592] check argument values --- SoftLayer/tests/CLI/modules/vs_tests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 609c5e9f4..9fae490d1 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -172,4 +172,10 @@ def test_check_create_args(self, confirm_mock): 'created': '2013-08-01 15:23:45'}], formatting.format_output(output, 'python')) service = self.client['Virtual_Guest'] - service.createObject.assert_called_with(maxMemory=1024, mask=mock.ANY) \ No newline at end of file + service.createObject.assert_called_with({'domain': 'example.com', + 'localDiskFlag': True, + 'startCpus': 2, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'maxMemory': 1024, + 'hourlyBillingFlag': True, + 'hostname': 'host'}) From 476755b83977ba309f10ba857fd00327f8e6fd21 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Tue, 12 Aug 2014 10:40:31 -0500 Subject: [PATCH 0043/2592] check argument values --- SoftLayer/tests/CLI/modules/vs_tests.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 9fae490d1..07acbc5ff 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -172,10 +172,11 @@ def test_check_create_args(self, confirm_mock): 'created': '2013-08-01 15:23:45'}], formatting.format_output(output, 'python')) service = self.client['Virtual_Guest'] - service.createObject.assert_called_with({'domain': 'example.com', - 'localDiskFlag': True, - 'startCpus': 2, - 'operatingSystemReferenceCode': 'UBUNTU_LATEST', - 'maxMemory': 1024, - 'hourlyBillingFlag': True, - 'hostname': 'host'}) + service.createObject.assert_called_with({ + 'domain': 'example.com', + 'localDiskFlag': True, + 'startCpus': 2, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'maxMemory': 1024, + 'hourlyBillingFlag': True, + 'hostname': 'host'}) From d449ae58ef61ab99faea54ddc9f9d1da1dd37634 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Tue, 12 Aug 2014 14:34:11 -0500 Subject: [PATCH 0044/2592] catch and ignore when exceptions are raised while retreiving reverse domain records --- SoftLayer/CLI/modules/server.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 5b663fe72..1e0d24d0b 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -200,14 +200,16 @@ def execute(self, args): table.add_row(['tags', formatting.listing(tag_row, separator=',')]) # Test to see if this actually has a primary (public) ip address - if result['primaryIpAddress']: - ptr_domains = (self.client['Hardware_Server'] - .getReverseDomainRecords(id=hardware_id)) - - for ptr_domain in ptr_domains: - for ptr in ptr_domain['resourceRecords']: - table.add_row(['ptr', ptr['data']]) - + try: + if result['primaryIpAddress']: + ptr_domains = (self.client['Hardware_Server'] + .getReverseDomainRecords(id=hardware_id)) + + for ptr_domain in ptr_domains: + for ptr in ptr_domain['resourceRecords']: + table.add_row(['ptr', ptr['data']]) + except: + pass return table From 57c093d4deea0549cbfd532eeca13c415b94e249 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Tue, 12 Aug 2014 15:50:01 -0500 Subject: [PATCH 0045/2592] handle non-existent reverse domains --- SoftLayer/CLI/modules/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 1e0d24d0b..4b52985c5 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -208,7 +208,7 @@ def execute(self, args): for ptr_domain in ptr_domains: for ptr in ptr_domain['resourceRecords']: table.add_row(['ptr', ptr['data']]) - except: + except Exception: pass return table From fc325ccfc33ca874963569dde72de321cc755e1c Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Wed, 13 Aug 2014 09:51:32 -0500 Subject: [PATCH 0046/2592] removing general exception --- SoftLayer/CLI/modules/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 4b52985c5..879ecd7ac 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -25,6 +25,7 @@ # :license: MIT, see LICENSE for more details. import os import re +import SoftLayer import SoftLayer from SoftLayer.CLI import environment @@ -208,7 +209,7 @@ def execute(self, args): for ptr_domain in ptr_domains: for ptr in ptr_domain['resourceRecords']: table.add_row(['ptr', ptr['data']]) - except Exception: + except SoftLayer.SoftLayerAPIError: pass return table From a532c530e29b166c7e6dd84ddcf3af001f660040 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Wed, 13 Aug 2014 10:10:11 -0500 Subject: [PATCH 0047/2592] removing general exception --- SoftLayer/CLI/modules/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 879ecd7ac..f7aa1d925 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -25,7 +25,6 @@ # :license: MIT, see LICENSE for more details. import os import re -import SoftLayer import SoftLayer from SoftLayer.CLI import environment From 899b2a94adc571cdd9ff34f0d21bd24a898d6627 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Wed, 13 Aug 2014 11:11:51 -0500 Subject: [PATCH 0048/2592] IMS checks privateNetworkOnlyFlag for reserver domains --- SoftLayer/CLI/modules/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index f7aa1d925..be390b386 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -201,7 +201,7 @@ def execute(self, args): # Test to see if this actually has a primary (public) ip address try: - if result['primaryIpAddress']: + if not result['privateNetworkOnlyFlag']: ptr_domains = (self.client['Hardware_Server'] .getReverseDomainRecords(id=hardware_id)) From 4b4dc64ff9b99caa3f72a1494a68f2b1d343cd6a Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Wed, 13 Aug 2014 11:35:13 -0500 Subject: [PATCH 0049/2592] changing test for issue 370 to sync it with IMS check --- SoftLayer/tests/CLI/modules/server_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index feb6dc556..32671126b 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -243,7 +243,7 @@ def test_server_details(self): def test_server_details_issue_332(self): runnable = server.ServerDetails(client=self.client) result = fixtures.Hardware_Server.getObject.copy() - result['primaryIpAddress'] = None + result['privateNetworkOnlyFlag'] = True self.client['Hardware_Server'].getObject.return_value = result runnable.execute({'': 1234, From c352245d0b31a9df9e01aa7d3fd1c3526ff59493 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 19 Aug 2014 17:02:57 -0500 Subject: [PATCH 0050/2592] Added check for privateNetworkOnly in vs to match server --- SoftLayer/CLI/modules/vs.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 6f5400855..1a5c5807e 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -208,13 +208,16 @@ def execute(self, args): table.add_row(['tags', formatting.listing(tag_row, separator=',')]) # Test to see if this actually has a primary (public) ip address - if result['primaryIpAddress']: - ptr_domains = (self.client['Virtual_Guest'] - .getReverseDomainRecords(id=vs_id)) - - for ptr_domain in ptr_domains: - for ptr in ptr_domain['resourceRecords']: - table.add_row(['ptr', ptr['data']]) + try: + if not result['privateNetworkOnlyFlag']: + ptr_domains = (self.client['Virtual_Guest'] + .getReverseDomainRecords(id=vs_id)) + + for ptr_domain in ptr_domains: + for ptr in ptr_domain['resourceRecords']: + table.add_row(['ptr', ptr['data']]) + except SoftLayer.SoftLayerAPIError: + pass return table From 7246780cbe8739e10db846b2d93f64bcafef2faf Mon Sep 17 00:00:00 2001 From: Scott Thompson Date: Thu, 21 Aug 2014 16:47:07 -0500 Subject: [PATCH 0051/2592] Fixes #381\nFirewall orders now take into account redundant interface pairs (or any other interface groups) when determining which firewall to order for a server --- SoftLayer/managers/firewall.py | 50 ++++++++++++++--- SoftLayer/testing/fixtures/Hardware_Server.py | 33 ++++++++++- SoftLayer/testing/fixtures/Virtual_Guest.py | 2 +- SoftLayer/tests/managers/firewall_tests.py | 56 +++++++++++++------ 4 files changed, 113 insertions(+), 28 deletions(-) diff --git a/SoftLayer/managers/firewall.py b/SoftLayer/managers/firewall.py index df6947bc5..9d04c8b9d 100644 --- a/SoftLayer/managers/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -49,16 +49,10 @@ def get_standard_package(self, server_id, is_cci=True): False for a server :returns: A dictionary containing the standard CCI firewall package """ - mask = ('mask[primaryNetworkComponent[speed]]') - if is_cci: - svc = self.client['Virtual_Guest'] - else: - svc = self.client['Hardware_Server'] - - item = svc.getObject(mask=mask, id=server_id) + firewall_port_speed = self._get_fwl_port_speed(server_id, is_cci) _filter = utils.NestedDict({}) - _value = "%s%s" % (item['primaryNetworkComponent']['speed'], + _value = "%s%s" % (firewall_port_speed, "Mbps Hardware Firewall") _filter['items']['description'] = utils.query_filter(_value) @@ -162,6 +156,46 @@ def _get_fwl_billing_item(self, firewall_id, dedicated=False): fwl_svc = self.client['Network_Component_Firewall'] return fwl_svc.getObject(id=firewall_id, mask=mask) + def _get_fwl_port_speed(self, server_id, is_cci=True): + fwl_port_speed = 0 + if is_cci: + mask = ('mask[primaryNetworkComponent[maxSpeed]]') + svc = self.client['Virtual_Guest'] + primary = svc.getObject(mask=mask, id=server_id) + fwl_port_speed = primary['primaryNetworkComponent']['maxSpeed'] + else: + mask = ('mask[id,maxSpeed,' + 'networkComponentGroup.networkComponents]') + svc = self.client['Hardware_Server'] + network_components = svc.getFrontendNetworkComponents( + mask=mask, id=server_id) + grouped = [interface['networkComponentGroup']['networkComponents'] + for interface in network_components + if 'networkComponentGroup' in interface] + ungrouped = [interface + for interface in network_components + if not('networkComponentGroup' in interface)] + + # For each group, sum the maxSpeeds of each compoment in the + # group. Put the sum for each in a new list + group_speeds = [] + for group in grouped: + group_speed = 0 + for interface in group: + group_speed += interface['maxSpeed'] + group_speeds.append(group_speed) + + # The max speed of all groups is the max of the list + max_grouped_speed = max(group_speeds) + + max_ungrouped = 0 + for interface in ungrouped: + max_ungrouped = max(max_ungrouped, interface['maxSpeed']) + + fwl_port_speed = max(max_grouped_speed, max_ungrouped) + + return fwl_port_speed + def get_firewalls(self): """ Returns a list of all firewalls on the account. diff --git a/SoftLayer/testing/fixtures/Hardware_Server.py b/SoftLayer/testing/fixtures/Hardware_Server.py index 98fb903f7..d5650cc85 100644 --- a/SoftLayer/testing/fixtures/Hardware_Server.py +++ b/SoftLayer/testing/fixtures/Hardware_Server.py @@ -23,7 +23,7 @@ 'primaryBackendIpAddress': '10.1.0.2', 'networkManagementIpAddress': '10.1.0.3', 'hardwareStatus': {'status': 'ACTIVE'}, - "primaryNetworkComponent": {"speed": 10}, + 'primaryNetworkComponent': {'maxSpeed': 10, 'speed': 10}, 'provisionDate': '2013-08-01 15:23:45', 'notes': 'These are test notes.', 'operatingSystem': { @@ -74,3 +74,34 @@ getReverseDomainRecords = [ {'resourceRecords': [{'data': '2.0.1.10.in-addr.arpa'}]}] bootToRescueLayer = True +getFrontendNetworkComponents = [ + {'maxSpeed': 100}, + { + 'maxSpeed': 1000, + 'networkComponentGroup': { + 'groupTypeId': 2, + 'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}] + } + }, + { + 'maxSpeed': 1000, + 'networkComponentGroup': { + 'groupTypeId': 2, + 'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}] + } + }, + { + 'maxSpeed': 1000, + 'networkComponentGroup': { + 'groupTypeId': 2, + 'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}] + } + }, + { + 'maxSpeed': 1000, + 'networkComponentGroup': { + 'groupTypeId': 2, + 'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}] + } + } +] diff --git a/SoftLayer/testing/fixtures/Virtual_Guest.py b/SoftLayer/testing/fixtures/Virtual_Guest.py index de9f16ca2..0ea9adeaa 100644 --- a/SoftLayer/testing/fixtures/Virtual_Guest.py +++ b/SoftLayer/testing/fixtures/Virtual_Guest.py @@ -23,7 +23,7 @@ 'primaryIpAddress': '172.16.240.2', 'globalIdentifier': '1a2b3c-1701', 'primaryBackendIpAddress': '10.45.19.37', - "primaryNetworkComponent": {"speed": 10}, + "primaryNetworkComponent": {"speed": 10, "maxSpeed": 100}, 'hourlyBillingFlag': False, 'createDate': '2013-08-01 15:23:45', 'blockDevices': [{"device": 0, "uuid": 1}, diff --git a/SoftLayer/tests/managers/firewall_tests.py b/SoftLayer/tests/managers/firewall_tests.py index 97d3d25b1..93e367832 100644 --- a/SoftLayer/tests/managers/firewall_tests.py +++ b/SoftLayer/tests/managers/firewall_tests.py @@ -55,35 +55,43 @@ def test_get_dedicated_fwl_rules(self): call.assert_called_once_with(id=1234, mask=MASK) self.assertEqual(rules, fixtures.Network_Vlan_Firewall.getRules) - def test_get_standard_package(self): + def test_get_standard_package_virtual_server(self): # test standard firewalls self.firewall.get_standard_package(server_id=1234, is_cci=True) - call2 = self.client['Virtual_Guest'].getObject - mask = ('mask[primaryNetworkComponent[speed]]') - f = self.client['Product_Package'].getItems + package_call = self.client['Product_Package'].getItems _filter = { 'items': { 'description': { - 'operation': '_= 10Mbps Hardware Firewall' + 'operation': '_= 100Mbps Hardware Firewall' } } } - f.assert_called_once_with(filter=_filter, id=0) + package_call.assert_called_once_with(filter=_filter, id=0) + + mask = ('mask[primaryNetworkComponent[maxSpeed]]') + call2 = self.client['Virtual_Guest'].getObject call2.assert_called_once_with(id=1234, mask=mask) + def test_get_standard_package_bare_metal(self): self.firewall.get_standard_package(server_id=1234, is_cci=False) - call2 = self.client['Hardware_Server'].getObject - mask = ('mask[primaryNetworkComponent[speed]]') + + # we should ask for the frontEndNetworkComponents to get + # the firewall port speed + mask = ('mask[id,maxSpeed,' + 'networkComponentGroup.networkComponents]') + fenc_call = self.client['Hardware_Server'].getFrontendNetworkComponents + fenc_call.assert_called_once_with(id=1234, mask=mask) + + # shiould call the product package for a 2000Mbps firwall f = self.client['Product_Package'].getItems _filter = { 'items': { 'description': { - 'operation': '_= 10Mbps Hardware Firewall' + 'operation': '_= 2000Mbps Hardware Firewall' } } } - f.assert_called_twice_with(filter=_filter, id=0) - call2.assert_called_once_with(id=1234, mask=mask) + f.assert_called_once_with(filter=_filter, id=0) def test_get_dedicated_package_ha(self): # test dedicated HA firewalls @@ -140,38 +148,50 @@ def test_add_standard_firewall_cci(self): _filter = { 'items': { 'description': { - 'operation': '_= 10Mbps Hardware Firewall' + 'operation': '_= 100Mbps Hardware Firewall' } } } f.assert_called_once_with(filter=_filter, id=0) call2 = self.client['Virtual_Guest'].getObject - mask = ('mask[primaryNetworkComponent[speed]]') - call2.assert_called_once_with(id=6327, mask=mask) + mask = ('mask[primaryNetworkComponent[maxSpeed]]') + call2.assert_called_once_with(id=server_id, mask=mask) f = self.client['Product_Order'].placeOrder f.assert_called_once() def test_add_standard_firewall_server(self): # test dedicated firewall for Servers server_id = 6327 - mask = ('mask[primaryNetworkComponent[speed]]') self.firewall.add_standard_firewall(server_id, is_cci=False) + + # The placeOrder call should be made at the end of the routine f = self.client['Product_Order'].placeOrder f.assert_called_once() + # We should query the product package for a 2000Mbps firewall f = self.client['Product_Package'].getItems _filter = { 'items': { 'description': { - 'operation': '_= 10Mbps Hardware Firewall' + 'operation': '_= 2000Mbps Hardware Firewall' } } } f.assert_called_once_with(filter=_filter, id=0) - call2 = self.client['Hardware_Server'].getObject - call2.assert_called_once_with(id=6327, mask=mask) + # we should ask for the frontEndNetworkComponents to get + # the firewall port speed + mask = ('mask[id,maxSpeed,' + 'networkComponentGroup.networkComponents]') + fenc_call = self.client['Hardware_Server'].getFrontendNetworkComponents + fenc_call.assert_called_once_with(id=server_id, mask=mask) + + def test__get_fwl_port_speed_server(self): + # Test the routine that calculates the speed of firewall + # required for a server + port_speed = self.firewall._get_fwl_port_speed(186908, False) + self.assertEqual(port_speed, 2000) def test_add_vlan_firewall(self): # test dedicated firewall for Vlan From 13420f20bd26dd8f79311aff27b6add7c788ca49 Mon Sep 17 00:00:00 2001 From: Scott Thompson Date: Thu, 21 Aug 2014 17:19:54 -0500 Subject: [PATCH 0052/2592] Removed unneeded parens and adde a docstring --- SoftLayer/managers/firewall.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/firewall.py b/SoftLayer/managers/firewall.py index 9d04c8b9d..ef9ad74f3 100644 --- a/SoftLayer/managers/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -157,6 +157,12 @@ def _get_fwl_billing_item(self, firewall_id, dedicated=False): return fwl_svc.getObject(id=firewall_id, mask=mask) def _get_fwl_port_speed(self, server_id, is_cci=True): + """ Determines the appropriate speed for a firewall + + :param int server_id: The ID of server the firewall is for + :param bool is_cci: true if the server_id is for a virtual server + :returns: a integer representing the Mbps speed of a firewall + """ fwl_port_speed = 0 if is_cci: mask = ('mask[primaryNetworkComponent[maxSpeed]]') @@ -174,7 +180,7 @@ def _get_fwl_port_speed(self, server_id, is_cci=True): if 'networkComponentGroup' in interface] ungrouped = [interface for interface in network_components - if not('networkComponentGroup' in interface)] + if 'networkComponentGroup' not in interface] # For each group, sum the maxSpeeds of each compoment in the # group. Put the sum for each in a new list From 879ae02e3e16139aeaa58c18f2af715b6d4d3d9a Mon Sep 17 00:00:00 2001 From: Scott Thompson Date: Fri, 22 Aug 2014 09:04:04 -0500 Subject: [PATCH 0053/2592] fixes #381. Additional commit to indicate a corrected build fixing this issue From c7dd64384608e55ca6388c773fc1aad7338cc213 Mon Sep 17 00:00:00 2001 From: Scott Thompson Date: Fri, 22 Aug 2014 09:11:23 -0500 Subject: [PATCH 0054/2592] fixes #381. Making pep8 happy From 943d9da9b09576844a9637ee22f41f57c9e77ad4 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 4 Sep 2014 11:13:56 -0500 Subject: [PATCH 0055/2592] Adding some basic DNS import support to the CLI --- SoftLayer/CLI/modules/dns.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 0d9d6f389..25ec0916b 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -8,6 +8,7 @@ delete Delete zone list List zones or a zone's records print Print zone in BIND format + import Import a BIND style zone file The available record commands are: add Add resource record @@ -80,6 +81,61 @@ def execute(self, args): raise exceptions.CLIAbort("Aborted.") +class ImportZone(environment.CLIRunnable): + """ +usage: sl dns import + +Creates a new zone based of a BIND formatted file + """ + action = 'import' + def execute(self,args): + import pprint + import re + import sys + manager = SoftLayer.DNSManager(self.client) + zone = '' + records = {} + pp = pprint.PrettyPrinter(indent=2) + lines = [line.strip() for line in open(args[''])] + zoneSearch = re.search('\$ORIGIN (?P.*)\.',lines[0]) + zone = zoneSearch.group('zone') + + + try: + zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') + except : + print "Unexpected error:", sys.exc_info()[0] + zone_id = None + pass + if (zone_id is None): + + print "CREATING ZONE: %s" % (zone) + manager.create_zone(zone) + zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') + + print "ZoneID: %s" % (zone_id) + + + + for content in lines: + domainSearch = re.search('(?P\w+)\s+(?P\d+)\s+(?P\w+)\s+(?P\w+)\s+(?P.*)',content) + if (domainSearch is not None): + domainName = domainSearch.group('domain') + domainttl = domainSearch.group('ttl') + domainClass = domainSearch.group('class') + domainType = domainSearch.group('type') + domainRecord = domainSearch.group('record') + print "Domain: %s TTL: %s Class: %s Type: %s Record: %s" % (domainName,domainttl,domainClass,domainType,domainRecord) + manager.create_record(zone_id,domainName,domainType,domainRecord,domainttl) + + continue + # print content + # zone = dns.zone.from_file(args['']) + print "IMPORT ZONE %s" % (args['']) + # pp.pprint(zone) + + + class ListZones(environment.CLIRunnable): """ usage: sl dns list [] [options] From 2a2ca0ff15acc8bafa7f59f1a116dc697c95bcb5 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 4 Sep 2014 12:41:19 -0500 Subject: [PATCH 0056/2592] Fixes test failures due to requests changes --- SoftLayer/tests/functional_tests.py | 6 ++--- SoftLayer/tests/transport_tests.py | 35 +++++++++++++++++++---------- SoftLayer/transports.py | 19 +++++++++------- tox.ini | 3 ++- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py index 025e59ed1..ed70fe877 100644 --- a/SoftLayer/tests/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -37,10 +37,8 @@ def test_no_hostname(self): # This test will fail if 'notvalidsoftlayer.com' becomes a thing SoftLayer.transports.make_xml_rpc_api_call( 'http://notvalidsoftlayer.com', 'getObject') - except SoftLayer.SoftLayerAPIError as e: - self.assertEqual(e.faultCode, 0) - self.assertIn('not known', e.faultString) - self.assertIn('not known', e.reason) + except Exception as ex: + self.assertIn('not known', str(ex)) else: self.fail('No Exception Raised') diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 196795dbc..5284e2bed 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -15,7 +15,8 @@ class TestXmlRpcAPICall(testing.TestCase): def set_up(self): - self.send_content = ''' + self.response = mock.MagicMock() + self.response.content = ''' @@ -26,9 +27,9 @@ def set_up(self): ''' - @mock.patch('SoftLayer.transports.requests.Session.send') - def test_call(self, send): - send().content = self.send_content + @mock.patch('requests.request') + def test_call(self, request): + request.return_value = self.response data = ''' @@ -46,33 +47,43 @@ def test_call(self, send): ''' resp = transports.make_xml_rpc_api_call( 'http://something.com/path/to/resource', 'getObject') - args = send.call_args + args = request.call_args self.assertIsNotNone(args) args, kwargs = args - send.assert_called_with(mock.ANY, proxies=None, timeout=None) + request.assert_called_with('POST', + 'http://something.com/path/to/resource', + headers=None, + proxies=None, + data=data, + timeout=None) self.assertEqual(resp, []) - self.assertEqual(args[0].body, data) def test_proxy_without_protocol(self): + # NOTE(sudorandom): This used to be an instance of requests.HTTPError, + # but something changes in requests to make that no + # longer the case. self.assertRaises( - SoftLayer.TransportError, + Exception, # NOQA transports.make_xml_rpc_api_call, 'http://something.com/path/to/resource', 'getObject', 'localhost:3128') - @mock.patch('SoftLayer.transports.requests.Session.send') - def test_valid_proxy(self, send): - send().content = self.send_content + @mock.patch('requests.request') + def test_valid_proxy(self, request): + request.return_value = self.response transports.make_xml_rpc_api_call( 'http://something.com/path/to/resource', 'getObject', proxy='http://localhost:3128') - send.assert_called_with( + request.assert_called_with( + 'POST', mock.ANY, + headers=None, proxies={'https': 'http://localhost:3128', 'http': 'http://localhost:3128'}, + data=mock.ANY, timeout=None) diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 364c60f24..44fc9812c 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -42,17 +42,16 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, payload = utils.xmlrpc_client.dumps(tuple(largs), methodname=method, allow_none=True) - session = requests.Session() - req = requests.Request('POST', uri, data=payload, - headers=http_headers).prepare() LOGGER.debug("=== REQUEST ===") LOGGER.info('POST %s', uri) - LOGGER.debug(req.headers) + LOGGER.debug(http_headers) LOGGER.debug(payload) - response = session.send(req, - timeout=timeout, - proxies=_proxies_dict(proxy)) + response = requests.request('POST', uri, + data=payload, + headers=http_headers, + timeout=timeout, + proxies=_proxies_dict(proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(response.headers) LOGGER.debug(response.content) @@ -91,14 +90,18 @@ def make_rest_api_call(method, url, :param dict http_headers: HTTP headers to use for the request :param int timeout: number of seconds to use as a timeout """ + LOGGER.debug("=== REQUEST ===") LOGGER.info('%s %s', method, url) + LOGGER.debug(http_headers) try: resp = requests.request(method, url, headers=http_headers, timeout=timeout, proxies=_proxies_dict(proxy)) - resp.raise_for_status() + LOGGER.debug("=== RESPONSE ===") + LOGGER.debug(resp.headers) LOGGER.debug(resp.content) + resp.raise_for_status() if url.endswith('.json'): return json.loads(resp.content) else: diff --git a/tox.ini b/tox.ini index 8e68e57e4..5abdf9e0d 100644 --- a/tox.ini +++ b/tox.ini @@ -21,10 +21,11 @@ deps = hacking pylint commands = - flake8 --max-complexity=36 --statistics \ + flake8 --max-complexity=36 \ --ignore=H401,H402,H404,H405 \ SoftLayer pylint SoftLayer \ + -r n \ # Don't show the long report --ignore=tests,testing \ -d R0903 \ # Too few public methods -d R0914 \ # Too many local variables From 61d62f2b67062ca5aaa804fbe855a3ed9379f04e Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 4 Sep 2014 12:49:32 -0500 Subject: [PATCH 0057/2592] Adds notes/references to the requests lib issue --- SoftLayer/tests/functional_tests.py | 3 +++ SoftLayer/tests/transport_tests.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py index ed70fe877..9f2af984d 100644 --- a/SoftLayer/tests/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -38,6 +38,9 @@ def test_no_hostname(self): SoftLayer.transports.make_xml_rpc_api_call( 'http://notvalidsoftlayer.com', 'getObject') except Exception as ex: + # NOTE(sudorandom): This used to be an instance of + # SoftLayer.SoftLayerAPIError + # Related issue: https://github.com/kennethreitz/requests/pull/2193 self.assertIn('not known', str(ex)) else: self.fail('No Exception Raised') diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 5284e2bed..bacd3c887 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -63,6 +63,8 @@ def test_proxy_without_protocol(self): # NOTE(sudorandom): This used to be an instance of requests.HTTPError, # but something changes in requests to make that no # longer the case. + # Related issue: + # https://github.com/kennethreitz/requests/pull/2193 self.assertRaises( Exception, # NOQA transports.make_xml_rpc_api_call, From 3e1c10487b80a642b1372ebb1b016923de6efe74 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 4 Sep 2014 17:23:27 -0500 Subject: [PATCH 0058/2592] Adds common CLI options at all levels --- SoftLayer/CLI/core.py | 41 +++++++++++++++++-------------- SoftLayer/CLI/modules/summary.py | 17 ++++++------- SoftLayer/managers/network.py | 3 +-- SoftLayer/tests/CLI/core_tests.py | 3 --- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index b3e33cf87..fa383eb74 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -69,6 +69,25 @@ VALID_FORMATS = ['raw', 'table', 'json'] +def _append_common_options(arg_doc): + """Append common options to the doc string""" + default_format = 'raw' + if sys.stdout.isatty(): + default_format = 'table' + + arg_doc += """ +Standard Options: + --format=ARG Output format. [Options: table, raw] [Default: %s] + -C FILE --config=FILE Config file location. [Default: ~/.softlayer] + --debug=LEVEL Specifies the debug noise level + 1=warn, 2=info, 3=debug + --timings Time each API call and display after results + --proxy=PROTO:PROXY_URL HTTP[s] proxy to be use to make API calls + -h --help Show this screen +""" % default_format + return arg_doc + + class CommandParser(object): """Helper class to parse commands. @@ -79,22 +98,17 @@ def __init__(self, env): def get_main_help(self): """Get main help text.""" - return __doc__.strip() + return _append_common_options(__doc__).strip() def get_module_help(self, module_name): """Get help text for a module.""" module = self.env.load_module(module_name) arg_doc = module.__doc__ - return arg_doc.strip() + return _append_common_options(arg_doc).strip() def get_command_help(self, module_name, command_name): """Get help text for a specific command.""" command = self.env.get_command(module_name, command_name) - - default_format = 'raw' - if sys.stdout.isatty(): - default_format = 'table' - arg_doc = command.__doc__ if 'confirm' in command.options: @@ -103,18 +117,7 @@ def get_command_help(self, module_name, command_name): -y, --really Confirm all prompt actions """ - if '[options]' in arg_doc: - arg_doc += """ -Standard Options: - --format=ARG Output format. [Options: table, raw] [Default: %s] - -C FILE --config=FILE Config file location. [Default: ~/.softlayer] - --debug=LEVEL Specifies the debug noise level - 1=warn, 2=info, 3=debug - --timings Time each API call and display after results - --proxy=PROTO:PROXY_URL HTTP[s] proxy to be use to make API calls - -h --help Show this screen -""" % default_format - return arg_doc.strip() + return _append_common_options(arg_doc).strip() def parse_main_args(self, args): """Parse root arguments.""" diff --git a/SoftLayer/CLI/modules/summary.py b/SoftLayer/CLI/modules/summary.py index 7c00aaf61..5a7630778 100644 --- a/SoftLayer/CLI/modules/summary.py +++ b/SoftLayer/CLI/modules/summary.py @@ -1,9 +1,14 @@ """ -usage: sl summary [options] +usage: sl summary [] [...] [options] Display summary information about the account + +Options: + --sortby=ARG Column to sort by. options: datacenter, vlans, + subnets, IPs, networking, hardware, vs """ # :license: MIT, see LICENSE for more details. +# pylint: disable=missing-docstring import SoftLayer from SoftLayer.CLI import environment @@ -11,15 +16,7 @@ class Summary(environment.CLIRunnable): - """ -usage: sl summary [options] - -Display summary information about the account - -Options: - --sortby=ARG Column to sort by. options: datacenter, vlans, - subnets, IPs, networking, hardware, vs -""" + __doc__ = __doc__ action = None def execute(self, args): diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 42667a342..f693f8893 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -334,8 +334,7 @@ def summary_by_datacenter(self): unique_network = [] for vlan in self.list_vlans(): - datacenter = vlan['primaryRouter']['datacenter'] - name = datacenter['name'] + name = utils.lookup(vlan, 'primaryRouter', 'datacenter', 'name') if name not in datacenters: datacenters[name] = { 'hardwareCount': 0, diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 1dca5290a..ce6056d34 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -197,7 +197,6 @@ def test_primary_help(self): args = self.parser.parse_main_args(args=[]) self.assertEqual({ '--help': False, - '-h': False, '': [], '': None, '': None, @@ -207,7 +206,6 @@ def test_primary_help(self): args = self.parser.parse_main_args(args=['help']) self.assertEqual({ '--help': False, - '-h': False, '': [], '': 'help', '': None, @@ -217,7 +215,6 @@ def test_primary_help(self): args = self.parser.parse_main_args(args=['help', 'module']) self.assertEqual({ '--help': False, - '-h': False, '': ['module'], '': 'help', '': None, From 0b6536d9d398b6c8cdfcd17f2f50702086b41648 Mon Sep 17 00:00:00 2001 From: Wolfgang Nagele Date: Thu, 4 Sep 2014 15:20:18 +1000 Subject: [PATCH 0059/2592] Fix for issue #383 --- SoftLayer/managers/hardware.py | 2 +- SoftLayer/managers/ordering.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 7f6de620c..0caed70df 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -224,7 +224,7 @@ def get_available_dedicated_server_packages(self): for package in packages: available_packages.append((package['id'], package['name'], - package['description'])) + package.get('description', None))) return available_packages diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 3942e7df3..d99d9dcbb 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -63,8 +63,8 @@ def filter_outlet_packages(packages): non_outlet_packages = [] for package in packages: - if all(['OUTLET' not in package['description'].upper(), - 'OUTLET' not in package['name'].upper()]): + if all(['OUTLET' not in package.get('description', '').upper(), + 'OUTLET' not in package.get('name', '').upper()]): non_outlet_packages.append(package) return non_outlet_packages From 354fcf486604b06384d3fac2f5a124f79313cf2e Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 5 Sep 2014 11:42:46 -0500 Subject: [PATCH 0060/2592] Adds CONTRIBUTORS file --- CONTRIBUTORS | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CONTRIBUTORS diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 000000000..88442234c --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,29 @@ +Amol Jadhav +Aparna Patil +Boden Russell +Brian Cline +chechuironman +Christopher Gallo +David Ibarra +Hans Kristian Moen +Jake Williams +Jason Johnson +Kevin Landreth +Kevin McDonald +Łukasz Oleś +Nathan Beittenmiller +Neetu Jain +Paul Sroufe +Phil Jackson +Robert Chumbley +Ryan Hanson +Scott Thompson +Sergio Carlos +Shane Poage +simplydave +SoftLayer +suppandi +Swapnil Khanapurkar +The SoftLayer Developer Network +Tim Ariyeh +Wissam Elriachy From e933f819a7e007e0b7afcb48cb68d63d56079f14 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 8 Sep 2014 12:31:40 -0500 Subject: [PATCH 0061/2592] handle the case of missing billingitem in subnet cancel --- SoftLayer/managers/network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 42667a342..536b6ba9f 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -132,8 +132,9 @@ def cancel_subnet(self, subnet_id): :param int subnet_id: The ID of the subnet to be cancelled. """ subnet = self.get_subnet(subnet_id, mask='id, billingItem.id') + if "billingItem" not in subnet: + raise ValueError('can not cancel subnet %s' % subnet_id) billing_id = subnet['billingItem']['id'] - return self.client['Billing_Item'].cancelService(id=billing_id) def edit_rwhois(self, abuse_email=None, address1=None, address2=None, From 2f7e0dc6f4ebaf8b645a978ecdfbaf0be98a0459 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 8 Sep 2014 12:52:44 -0500 Subject: [PATCH 0062/2592] changing error raised --- SoftLayer/managers/network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 536b6ba9f..d3b299930 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -7,6 +7,7 @@ """ from SoftLayer import utils +from SoftLayer import exceptions DEFAULT_SUBNET_MASK = ','.join(['hardware', 'datacenter', @@ -133,7 +134,7 @@ def cancel_subnet(self, subnet_id): """ subnet = self.get_subnet(subnet_id, mask='id, billingItem.id') if "billingItem" not in subnet: - raise ValueError('can not cancel subnet %s' % subnet_id) + raise exceptions.SoftLayerError('Can not cancel subnet %s' % subnet_id) billing_id = subnet['billingItem']['id'] return self.client['Billing_Item'].cancelService(id=billing_id) From fd16472f1c358a5ad000dcb68bb32e628bb07675 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 8 Sep 2014 13:06:51 -0500 Subject: [PATCH 0063/2592] changing error raised --- SoftLayer/managers/network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index d3b299930..4c2611954 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -6,8 +6,8 @@ :license: MIT, see LICENSE for more details. """ -from SoftLayer import utils from SoftLayer import exceptions +from SoftLayer import utils DEFAULT_SUBNET_MASK = ','.join(['hardware', 'datacenter', @@ -134,7 +134,8 @@ def cancel_subnet(self, subnet_id): """ subnet = self.get_subnet(subnet_id, mask='id, billingItem.id') if "billingItem" not in subnet: - raise exceptions.SoftLayerError('Can not cancel subnet %s' % subnet_id) + raise exceptions.SoftLayerError('Can not cancel subnet %' + % subnet_id) billing_id = subnet['billingItem']['id'] return self.client['Billing_Item'].cancelService(id=billing_id) From b8515da3c5b2dc59dbaa247494c35446d380b794 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 8 Sep 2014 13:21:26 -0500 Subject: [PATCH 0064/2592] changing error raised --- SoftLayer/managers/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 4c2611954..64b1fcebe 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -134,8 +134,8 @@ def cancel_subnet(self, subnet_id): """ subnet = self.get_subnet(subnet_id, mask='id, billingItem.id') if "billingItem" not in subnet: - raise exceptions.SoftLayerError('Can not cancel subnet %' - % subnet_id) + raise exceptions.SoftLayerError('Can not cancel ' + 'subnet %' % subnet_id) billing_id = subnet['billingItem']['id'] return self.client['Billing_Item'].cancelService(id=billing_id) From 172df2b2ac9e76df6efb43ebf79d6dc5bdd6cd4a Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Mon, 8 Sep 2014 13:37:35 -0500 Subject: [PATCH 0065/2592] pep8 happy --- SoftLayer/managers/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 64b1fcebe..fcc6e28e6 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -134,8 +134,8 @@ def cancel_subnet(self, subnet_id): """ subnet = self.get_subnet(subnet_id, mask='id, billingItem.id') if "billingItem" not in subnet: - raise exceptions.SoftLayerError('Can not cancel ' - 'subnet %' % subnet_id) + raise exceptions.SoftLayerError('Can not cancel subnet %' % + subnet_id) billing_id = subnet['billingItem']['id'] return self.client['Billing_Item'].cancelService(id=billing_id) From 936f3065a730b7390644136e1d673695e0e504b1 Mon Sep 17 00:00:00 2001 From: Neetu Jain Date: Tue, 9 Sep 2014 12:01:12 -0500 Subject: [PATCH 0066/2592] pep8 happy --- SoftLayer/managers/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index fcc6e28e6..d337c6f5a 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -134,8 +134,8 @@ def cancel_subnet(self, subnet_id): """ subnet = self.get_subnet(subnet_id, mask='id, billingItem.id') if "billingItem" not in subnet: - raise exceptions.SoftLayerError('Can not cancel subnet %' % - subnet_id) + raise exceptions.SoftLayerError("subnet %s can not be cancelled" + " " % subnet_id) billing_id = subnet['billingItem']['id'] return self.client['Billing_Item'].cancelService(id=billing_id) From a61500e62981c11f9c0403aebe68e12112758699 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 9 Sep 2014 15:31:18 -0500 Subject: [PATCH 0067/2592] Reverts tests because Requests 2.4.1 was released --- SoftLayer/tests/functional_tests.py | 7 +++---- SoftLayer/tests/transport_tests.py | 7 +------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py index 9f2af984d..d121f9958 100644 --- a/SoftLayer/tests/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -37,11 +37,10 @@ def test_no_hostname(self): # This test will fail if 'notvalidsoftlayer.com' becomes a thing SoftLayer.transports.make_xml_rpc_api_call( 'http://notvalidsoftlayer.com', 'getObject') - except Exception as ex: - # NOTE(sudorandom): This used to be an instance of - # SoftLayer.SoftLayerAPIError - # Related issue: https://github.com/kennethreitz/requests/pull/2193 + except SoftLayer.SoftLayerAPIError as ex: self.assertIn('not known', str(ex)) + self.assertIn('not known', ex.faultString) + self.assertEqual(ex.faultCode, 0) else: self.fail('No Exception Raised') diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index bacd3c887..a0e82f558 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -60,13 +60,8 @@ def test_call(self, request): self.assertEqual(resp, []) def test_proxy_without_protocol(self): - # NOTE(sudorandom): This used to be an instance of requests.HTTPError, - # but something changes in requests to make that no - # longer the case. - # Related issue: - # https://github.com/kennethreitz/requests/pull/2193 self.assertRaises( - Exception, # NOQA + SoftLayer.TransportError, # NOQA transports.make_xml_rpc_api_call, 'http://something.com/path/to/resource', 'getObject', From 68a667a4067415a04a37975533e5e489eeaae094 Mon Sep 17 00:00:00 2001 From: underscorephil Date: Wed, 10 Sep 2014 11:33:05 -0500 Subject: [PATCH 0068/2592] Fix doc error --- SoftLayer/CLI/modules/vs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/vs.py b/SoftLayer/CLI/modules/vs.py index 81a476b8b..9d5a1ef55 100755 --- a/SoftLayer/CLI/modules/vs.py +++ b/SoftLayer/CLI/modules/vs.py @@ -1104,7 +1104,7 @@ class UpgradeVS(environment.CLIRunnable): Upgrade parameters of a virtual server Examples: - sl vs upgrade --cpus 2 + sl vs upgrade --cpu 2 sl vs upgrade --memory 2048 --network 1000 Options: --cpu=CPU Number of CPU cores From 8d6270fdc559aa1e209a812658ff4f85d30c9a75 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 10 Sep 2014 14:42:49 -0500 Subject: [PATCH 0069/2592] Adds more details to image commands --- SoftLayer/CLI/formatting.py | 9 +++++++ SoftLayer/CLI/modules/image.py | 49 +++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index c8facc606..8c2578f28 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -104,6 +104,15 @@ def mb_to_gb(megabytes): return FormattedItem(megabytes, "%dG" % (float(megabytes) / 1024)) +def b_to_gb(_bytes): + """Converts number of bytes to a FormattedItem in gigabytes. + + :param int _bytes: number of bytes + """ + return FormattedItem(_bytes, + "%.2fG" % (float(_bytes) / 1024 / 1024 / 1024)) + + def gb(gigabytes): # pylint: disable=C0103 """Converts number of gigabytes to a FormattedItem in gigabytes. diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py index 7223749f9..607561024 100644 --- a/SoftLayer/CLI/modules/image.py +++ b/SoftLayer/CLI/modules/image.py @@ -16,6 +16,14 @@ from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +from SoftLayer import utils + +MASK = ('id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,' + 'imageType') +DETAIL_MASK = MASK + (',children[id,blockDevicesDiskSpaceTotal,datacenter],' + 'note') +PUBLIC_TYPE = formatting.FormattedItem('PUBLIC', 'Public') +PRIVATE_TYPE = formatting.FormattedItem('PRIVATE', 'Private') class ListImages(environment.CLIRunnable): @@ -34,32 +42,34 @@ def execute(self, args): image_mgr = SoftLayer.ImageManager(self.client) neither = not any([args['--private'], args['--public']]) - mask = 'id,accountId,name,globalIdentifier,blockDevices,parentId' images = [] if args['--private'] or neither: - for image in image_mgr.list_private_images(mask=mask): - image['visibility'] = 'private' + for image in image_mgr.list_private_images(mask=MASK): images.append(image) if args['--public'] or neither: - for image in image_mgr.list_public_images(mask=mask): - image['visibility'] = 'public' + for image in image_mgr.list_public_images(mask=MASK): images.append(image) table = formatting.Table(['id', 'account', - 'visibility', 'name', + 'type', + 'visibility', 'global_identifier']) images = [image for image in images if image['parentId'] == ''] for image in images: + table.add_row([ image['id'], image.get('accountId', formatting.blank()), - image['visibility'], image['name'].strip(), + formatting.FormattedItem( + utils.lookup(image, 'imageType', 'keyName'), + utils.lookup(image, 'imageType', 'name')), + PUBLIC_TYPE if image['publicFlag'] else PRIVATE_TYPE, image.get('globalIdentifier', formatting.blank()), ]) @@ -80,17 +90,36 @@ def execute(self, args): args.get(''), 'image') - image = image_mgr.get_image(image_id) + image = image_mgr.get_image(image_id, mask=DETAIL_MASK) table = formatting.KeyValueTable(['Name', 'Value']) table.align['Name'] = 'r' table.align['Value'] = 'l' table.add_row(['id', image['id']]) - table.add_row(['account', image.get('accountId', formatting.blank())]) - table.add_row(['name', image['name'].strip()]) table.add_row(['global_identifier', image.get('globalIdentifier', formatting.blank())]) + table.add_row(['name', image['name'].strip()]) + table.add_row(['account', image.get('accountId', formatting.blank())]) + table.add_row(['visibility', + PUBLIC_TYPE if image['publicFlag'] else PRIVATE_TYPE]) + table.add_row(['type', + formatting.FormattedItem( + utils.lookup(image, 'imageType', 'keyName'), + utils.lookup(image, 'imageType', 'name'), + )]) + table.add_row(['flex', image.get('flexImageFlag')]) + table.add_row(['note', image.get('note')]) + disk_space = 0 + datacenters = [] + for child in image.get('children'): + disk_space = child.get('blockDevicesDiskSpaceTotal', 0) + if child.get('datacenter'): + datacenters.append(utils.lookup(child, 'datacenter', 'name')) + + table.add_row(['disk_space', formatting.b_to_gb(disk_space)]) + table.add_row(['datacenters', formatting.listing(datacenters, + separator=',')]) return table From 4fca17ef3f55c6fe7f77e9b22bc66086bb81c9f7 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 10 Sep 2014 14:45:15 -0500 Subject: [PATCH 0070/2592] Makes disk space an int --- SoftLayer/CLI/modules/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py index 607561024..e74bdc6d0 100644 --- a/SoftLayer/CLI/modules/image.py +++ b/SoftLayer/CLI/modules/image.py @@ -113,7 +113,7 @@ def execute(self, args): disk_space = 0 datacenters = [] for child in image.get('children'): - disk_space = child.get('blockDevicesDiskSpaceTotal', 0) + disk_space = int(child.get('blockDevicesDiskSpaceTotal', 0)) if child.get('datacenter'): datacenters.append(utils.lookup(child, 'datacenter', 'name')) From be20777b062161c32cbf851c70c7d4d526cc0699 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 11 Sep 2014 11:16:24 -0500 Subject: [PATCH 0071/2592] Small cleanup. Adds creation date --- SoftLayer/CLI/modules/image.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py index e74bdc6d0..653eeea1d 100644 --- a/SoftLayer/CLI/modules/image.py +++ b/SoftLayer/CLI/modules/image.py @@ -21,7 +21,7 @@ MASK = ('id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,' 'imageType') DETAIL_MASK = MASK + (',children[id,blockDevicesDiskSpaceTotal,datacenter],' - 'note') + 'note,createDate') PUBLIC_TYPE = formatting.FormattedItem('PUBLIC', 'Public') PRIVATE_TYPE = formatting.FormattedItem('PRIVATE', 'Private') @@ -91,6 +91,12 @@ def execute(self, args): 'image') image = image_mgr.get_image(image_id, mask=DETAIL_MASK) + disk_space = 0 + datacenters = [] + for child in image.get('children'): + disk_space = int(child.get('blockDevicesDiskSpaceTotal', 0)) + if child.get('datacenter'): + datacenters.append(utils.lookup(child, 'datacenter', 'name')) table = formatting.KeyValueTable(['Name', 'Value']) table.align['Name'] = 'r' @@ -110,13 +116,7 @@ def execute(self, args): )]) table.add_row(['flex', image.get('flexImageFlag')]) table.add_row(['note', image.get('note')]) - disk_space = 0 - datacenters = [] - for child in image.get('children'): - disk_space = int(child.get('blockDevicesDiskSpaceTotal', 0)) - if child.get('datacenter'): - datacenters.append(utils.lookup(child, 'datacenter', 'name')) - + table.add_row(['created', image.get('createDate')]) table.add_row(['disk_space', formatting.b_to_gb(disk_space)]) table.add_row(['datacenters', formatting.listing(datacenters, separator=',')]) From 29c23744ac480f50592b8f6f24b650445f01f072 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 11 Sep 2014 13:34:33 -0500 Subject: [PATCH 0072/2592] Adds image status to image detail command --- SoftLayer/CLI/modules/image.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py index 653eeea1d..9ef6b329c 100644 --- a/SoftLayer/CLI/modules/image.py +++ b/SoftLayer/CLI/modules/image.py @@ -21,7 +21,7 @@ MASK = ('id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,' 'imageType') DETAIL_MASK = MASK + (',children[id,blockDevicesDiskSpaceTotal,datacenter],' - 'note,createDate') + 'note,createDate,status') PUBLIC_TYPE = formatting.FormattedItem('PUBLIC', 'Public') PRIVATE_TYPE = formatting.FormattedItem('PRIVATE', 'Private') @@ -106,6 +106,10 @@ def execute(self, args): table.add_row(['global_identifier', image.get('globalIdentifier', formatting.blank())]) table.add_row(['name', image['name'].strip()]) + table.add_row(['status', formatting.FormattedItem( + utils.lookup(image, 'status', 'keyname'), + utils.lookup(image, 'status', 'name'), + )]) table.add_row(['account', image.get('accountId', formatting.blank())]) table.add_row(['visibility', PUBLIC_TYPE if image['publicFlag'] else PRIVATE_TYPE]) From 9c5e4cfcf17f958a798573188074d64f37792190 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 11 Sep 2014 14:07:01 -0500 Subject: [PATCH 0073/2592] Sorts datacenter list in image detail by name --- SoftLayer/CLI/modules/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py index 9ef6b329c..5075eed60 100644 --- a/SoftLayer/CLI/modules/image.py +++ b/SoftLayer/CLI/modules/image.py @@ -122,7 +122,7 @@ def execute(self, args): table.add_row(['note', image.get('note')]) table.add_row(['created', image.get('createDate')]) table.add_row(['disk_space', formatting.b_to_gb(disk_space)]) - table.add_row(['datacenters', formatting.listing(datacenters, + table.add_row(['datacenters', formatting.listing(sorted(datacenters), separator=',')]) return table From 81323523066caa66a0e6525d507ae8a75f5a32e3 Mon Sep 17 00:00:00 2001 From: Wolfgang Nagele Date: Fri, 5 Sep 2014 21:30:17 +1000 Subject: [PATCH 0074/2592] Support immediate cancellation of servers --- SoftLayer/CLI/modules/server.py | 5 ++++- SoftLayer/managers/hardware.py | 5 +++-- SoftLayer/tests/CLI/modules/server_tests.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index be390b386..e49891991 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -252,6 +252,8 @@ class CancelServer(environment.CLIRunnable): Cancel a dedicated server Options: + --immediate Cancels the server immediately (instead of on the billing + anniversary) --comment=COMMENT An optional comment to add to the cancellation ticket --reason=REASON An optional cancellation reason. See cancel-reasons for a list of available options @@ -271,9 +273,10 @@ def execute(self, args): comment = self.env.input("(Optional) Add a cancellation comment:") reason = args.get('--reason') + immediate = args.get('--immediate') if args['--really'] or formatting.no_going_back(hw_id): - mgr.cancel_hardware(hw_id, reason, comment) + mgr.cancel_hardware(hw_id, reason, comment, immediate) else: raise exceptions.CLIAbort('Aborted') diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 0caed70df..2f8addd40 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -33,7 +33,8 @@ def __init__(self, client, ordering_manager=None): else: self.ordering_manager = ordering_manager - def cancel_hardware(self, hardware_id, reason='unneeded', comment=''): + def cancel_hardware(self, hardware_id, reason='unneeded', comment='', + immediate=False): """ Cancels the specified dedicated server. :param int hardware_id: The ID of the hardware to be cancelled. @@ -48,7 +49,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment=''): mask='id,bareMetalInstanceFlag') if server.get('bareMetalInstanceFlag'): - return self.cancel_metal(hardware_id) + return self.cancel_metal(hardware_id, immediate) reasons = self.get_cancellation_reasons() cancel_reason = reasons['unneeded'] diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index 32671126b..29b6f14a5 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -329,7 +329,7 @@ def test_cancel_server(self, resolve_mock, cancel_mock, ngb_mock): args = {'--really': True, '--reason': 'Test'} runnable.execute(args) - cancel_mock.assert_called_with(hw_id, args['--reason'], None) + cancel_mock.assert_called_with(hw_id, args['--reason'], None, None) # Now check to make sure we properly call CLIAbort in the negative case env_mock = mock.Mock() From bce391da0d51c359c0a1739c82c3da91fa847caa Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 12 Sep 2014 17:52:53 -0500 Subject: [PATCH 0075/2592] cleaning up some code --- SoftLayer/CLI/modules/dns.py | 77 +++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 31 deletions(-) mode change 100755 => 100644 SoftLayer/CLI/modules/dns.py diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py old mode 100755 new mode 100644 index 25ec0916b..96e43ee9c --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -83,57 +83,72 @@ def execute(self, args): class ImportZone(environment.CLIRunnable): """ -usage: sl dns import +usage: sl dns import [--dryRun] + +Creates a new zone based off a nicely BIND formatted file + +Arguments: + Path to the bind zone file you want to import +Options: + --dryRun don't actually do anything. This will show you what we were able to parse. -Creates a new zone based of a BIND formatted file """ action = 'import' def execute(self,args): - import pprint import re - import sys + dryRun = args.get('--dryRun') + manager = SoftLayer.DNSManager(self.client) - zone = '' - records = {} - pp = pprint.PrettyPrinter(indent=2) lines = [line.strip() for line in open(args[''])] zoneSearch = re.search('\$ORIGIN (?P.*)\.',lines[0]) zone = zoneSearch.group('zone') - - try: - zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') - except : - print "Unexpected error:", sys.exc_info()[0] - zone_id = None - pass - if (zone_id is None): - - print "CREATING ZONE: %s" % (zone) - manager.create_zone(zone) - zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') - - print "ZoneID: %s" % (zone_id) - - + if (dryRun): + print "Starting up a dry run..." + zone_id = 0 + else: + try: + zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') + except : + print "\033[92mCREATED ZONE: %s\033[0m" % (zone) + manager.create_zone(zone) + zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') for content in lines: - domainSearch = re.search('(?P\w+)\s+(?P\d+)\s+(?P\w+)\s+(?P\w+)\s+(?P.*)',content) + domainSearch = re.search('((?P(\w+(\.)?)*|\@)?\s+(?P\d+)?\s+(?P\w+)?)?\s+(?P\w+)\s+(?P.*)',content) if (domainSearch is not None): domainName = domainSearch.group('domain') + #The API requires we send a host, although bind allows a blank entry. @ is the same thing as blank + if (domainName is None): + domainName = "@" + domainttl = domainSearch.group('ttl') domainClass = domainSearch.group('class') domainType = domainSearch.group('type') domainRecord = domainSearch.group('record') - print "Domain: %s TTL: %s Class: %s Type: %s Record: %s" % (domainName,domainttl,domainClass,domainType,domainRecord) - manager.create_record(zone_id,domainName,domainType,domainRecord,domainttl) - continue - # print content - # zone = dns.zone.from_file(args['']) - print "IMPORT ZONE %s" % (args['']) - # pp.pprint(zone) + #This will skip the SOA record bit. And any domain that gets parsed oddly. + if (domainType.upper() == 'IN'): + print "SKIPPED: Host: %s TTL: %s Type: %s Record: %s" % (domainName,domainttl,domainType,domainRecord) + continue + + #the dns class doesn't support weighted MX records yet, so we chomp that part out. + if (domainType.upper() == "MX"): + recordSearch = re.search('(?P\d+)\s+(?P.*)',domainRecord) + domainRecord = recordSearch.group('record') + + try: + if (dryRun): + print "Parsed: Host: %s TTL: %s Type: %s Record: %s" % (domainName,domainttl,domainType,domainRecord) + else: + manager.create_record(zone_id,domainName,domainType,domainRecord,domainttl) + print "\033[92mCreated: Host: %s TTL: %s Type: %s Record: %s\033[0m" % (domainName,domainttl,domainType,domainRecord) + except Exception, e: + print "\033[91mFAILED: Host: %s Type: %s Record: %s" % (domainName,domainType,domainRecord.upper()) + print "\t", e ,"\033[0m" + + return "Finished" class ListZones(environment.CLIRunnable): From c0470894ff1b68ff521bd09c5bb7cf86f4d7291d Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 10 Sep 2014 11:42:39 -0500 Subject: [PATCH 0076/2592] Makes Authentication More Flexible --- SoftLayer/API.py | 20 ++++++++++++------- SoftLayer/auth.py | 32 ++++++++++++++---------------- SoftLayer/tests/api_tests.py | 6 +++--- SoftLayer/tests/auth_tests.py | 32 ++++++++++++++++++------------ SoftLayer/tests/transport_tests.py | 8 ++++++-- SoftLayer/transports.py | 13 ++++++++---- 6 files changed, 65 insertions(+), 46 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 61851a35a..f2d8bf75c 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -146,9 +146,6 @@ def call(self, service, method, *args, **kwargs): headers = kwargs.get('headers', {}) - if self.auth: - headers.update(self.auth.get_headers()) - if kwargs.get('id') is not None: headers[service + 'InitParameters'] = {'id': kwargs.get('id')} @@ -178,11 +175,18 @@ def call(self, service, method, *args, **kwargs): http_headers.update(kwargs.get('raw_headers')) uri = '/'.join([self.endpoint_url, service]) + options = { + 'headers': headers, + 'http_headers': http_headers, + 'timeout': self.timeout, + 'proxy': self.proxy, + } + + if self.auth: + options = self.auth.get_options(options) + return transports.make_xml_rpc_api_call(uri, method, args, - headers=headers, - http_headers=http_headers, - timeout=self.timeout, - proxy=self.proxy) + **options) __call__ = call @@ -326,6 +330,8 @@ def call(self, name, *args, **kwargs): :param int offset: (optional) offset results by this many :param boolean iter: (optional) if True, returns a generator with the results + :param bool verify: verify SSL cert + :param cert: client certificate path Usage: >>> import SoftLayer diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index e430839a7..a0b1daf0b 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -10,8 +10,8 @@ class AuthenticationBase(object): """A base authentication class intended to be overridden.""" - def get_headers(self): - """Return a dictionary of headers to be inserted for authentication.""" + def get_options(self, options): + """Receives request options and returns request options.""" raise NotImplementedError @@ -26,15 +26,14 @@ def __init__(self, user_id, auth_token): self.user_id = user_id self.auth_token = auth_token - def get_headers(self): - """Returns token-based auth headers.""" - return { - 'authenticate': { - 'complexType': 'PortalLoginToken', - 'userId': self.user_id, - 'authToken': self.auth_token, - } + def get_options(self, options): + """Sets token-based auth headers.""" + options['headers']['authenticate'] = { + 'complexType': 'PortalLoginToken', + 'userId': self.user_id, + 'authToken': self.auth_token, } + return options def __repr__(self): return "" % (self.user_id, self.auth_token) @@ -50,14 +49,13 @@ def __init__(self, username, api_key): self.username = username self.api_key = api_key - def get_headers(self): - """Returns token-based auth headers.""" - return { - 'authenticate': { - 'username': self.username, - 'apiKey': self.api_key, - } + def get_options(self, options): + """Sets token-based auth headers.""" + options['headers']['authenticate'] = { + 'username': self.username, + 'apiKey': self.api_key, } + return options def __repr__(self): return "" % (self.username) diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index d9408cdc9..8260f4e62 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -17,9 +17,9 @@ def test_init(self): client = SoftLayer.Client(username='doesnotexist', api_key='issurelywrong', timeout=10) - auth_headers = {'authenticate': {'username': 'doesnotexist', - 'apiKey': 'issurelywrong'}} - self.assertEqual(client.auth.get_headers(), auth_headers) + self.assertIsInstance(client.auth, SoftLayer.BasicAuthentication) + self.assertEqual(client.auth.username, 'doesnotexist') + self.assertEqual(client.auth.api_key, 'issurelywrong') self.assertEqual(client.endpoint_url, SoftLayer.API_PUBLIC_ENDPOINT.rstrip('/')) self.assertEqual(client.timeout, 10) diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py index 2a3af4b1f..3e1fe74ee 100644 --- a/SoftLayer/tests/auth_tests.py +++ b/SoftLayer/tests/auth_tests.py @@ -9,9 +9,9 @@ class TestAuthenticationBase(testing.TestCase): - def test_get_headers(self): + def test_get_options(self): auth_base = auth.AuthenticationBase() - self.assertRaises(NotImplementedError, auth_base.get_headers) + self.assertRaises(NotImplementedError, auth_base.get_options, {}) class TestBasicAuthentication(testing.TestCase): @@ -22,11 +22,14 @@ def test_attribs(self): self.assertEqual(self.auth.username, 'USERNAME') self.assertEqual(self.auth.api_key, 'APIKEY') - def test_get_headers(self): - self.assertEqual(self.auth.get_headers(), { - 'authenticate': { - 'username': 'USERNAME', - 'apiKey': 'APIKEY', + def test_get_options(self): + headers = {'headers': {}} + self.assertEqual(self.auth.get_options(headers), { + 'headers': { + 'authenticate': { + 'username': 'USERNAME', + 'apiKey': 'APIKEY', + } } }) @@ -44,12 +47,15 @@ def test_attribs(self): self.assertEqual(self.auth.user_id, 12345) self.assertEqual(self.auth.auth_token, 'TOKEN') - def test_get_headers(self): - self.assertEqual(self.auth.get_headers(), { - 'authenticate': { - 'complexType': 'PortalLoginToken', - 'userId': 12345, - 'authToken': 'TOKEN', + def test_get_options(self): + headers = {'headers': {}} + self.assertEqual(self.auth.get_options(headers), { + 'headers': { + 'authenticate': { + 'complexType': 'PortalLoginToken', + 'userId': 12345, + 'authToken': 'TOKEN', + } } }) diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index a0e82f558..e65ca6595 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -56,7 +56,9 @@ def test_call(self, request): headers=None, proxies=None, data=data, - timeout=None) + timeout=None, + cert=None, + verify=True) self.assertEqual(resp, []) def test_proxy_without_protocol(self): @@ -81,7 +83,9 @@ def test_valid_proxy(self, request): proxies={'https': 'http://localhost:3128', 'http': 'http://localhost:3128'}, data=mock.ANY, - timeout=None) + timeout=None, + cert=None, + verify=True) class TestRestAPICall(testing.TestCase): diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 44fc9812c..332ae9a98 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -23,8 +23,9 @@ def _proxies_dict(proxy): return {'http': proxy, 'https': proxy} -def make_xml_rpc_api_call(uri, method, args=None, headers=None, - http_headers=None, timeout=None, proxy=None): +def make_xml_rpc_api_call(url, method, args=None, headers=None, + http_headers=None, timeout=None, proxy=None, + verify=True, cert=None): """Makes a SoftLayer API call against the XML-RPC endpoint. :param string uri: endpoint URL @@ -32,6 +33,8 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, :param dict headers: XML-RPC headers to use for the request :param dict http_headers: HTTP headers to use for the request :param int timeout: number of seconds to use as a timeout + :param bool verify: verify SSL cert + :param cert: client certificate path """ if args is None: args = tuple() @@ -43,14 +46,16 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, methodname=method, allow_none=True) LOGGER.debug("=== REQUEST ===") - LOGGER.info('POST %s', uri) + LOGGER.info('POST %s', url) LOGGER.debug(http_headers) LOGGER.debug(payload) - response = requests.request('POST', uri, + response = requests.request('POST', url, data=payload, headers=http_headers, timeout=timeout, + verify=verify, + cert=cert, proxies=_proxies_dict(proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(response.headers) From 373e1cb5d1864e2947bb684b831ed0043e4bc891 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 15 Sep 2014 14:20:21 -0500 Subject: [PATCH 0077/2592] added cli test for zone import --- .../tests/CLI/modules/artifacts/realtest.com | 21 +++++++++++++++++++ SoftLayer/tests/CLI/modules/dns_tests.py | 12 +++++++++++ 2 files changed, 33 insertions(+) create mode 100644 SoftLayer/tests/CLI/modules/artifacts/realtest.com diff --git a/SoftLayer/tests/CLI/modules/artifacts/realtest.com b/SoftLayer/tests/CLI/modules/artifacts/realtest.com new file mode 100644 index 000000000..0c5550d1d --- /dev/null +++ b/SoftLayer/tests/CLI/modules/artifacts/realtest.com @@ -0,0 +1,21 @@ +$ORIGIN realtest.com. +$TTL 86400 +@ IN SOA ns1.softlayer.com. support.softlayer.com. ( + 2014052300 ; Serial + 7200 ; Refresh + 600 ; Retry + 1728000 ; Expire + 43200) ; Minimum + +@ 86400 IN NS ns1.softlayer.com. +@ 86400 IN NS ns2.softlayer.com. + + IN MX 10 test.realtest.com. +testing 86400 IN A 127.0.0.1 +testing1 86400 IN A 12.12.0.1 +server2 IN A 1.0.3.4 +ftp IN CNAME server2 +dev.realtest.com IN TXT "This is just a test of the txt record" + IN AAAA 2001:db8:10::1 +spf IN TXT "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all" + diff --git a/SoftLayer/tests/CLI/modules/dns_tests.py b/SoftLayer/tests/CLI/modules/dns_tests.py index 811556db2..b4986a0d5 100644 --- a/SoftLayer/tests/CLI/modules/dns_tests.py +++ b/SoftLayer/tests/CLI/modules/dns_tests.py @@ -119,3 +119,15 @@ def test_delete_record(self, no_going_back_mock): '': 'hostname', '--id': 1, '--really': False}) + + def test_import_zone(self): + import pprint + pp = pprint.PrettyPrinter(indent=4) + + command = dns.ImportZone(client=self.client) + output = command.execute({ + '' : 'realtest.com' + '--dryRun' : '--dryRun' + }) + pp.pprint(output) + self.assertEqual(['Finished'],output) \ No newline at end of file From e115be7233cfb56f87ea3b0a04f446f44b5b0067 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 18 Sep 2014 18:18:03 -0500 Subject: [PATCH 0078/2592] Adds backwards compat for old style auth objects --- SoftLayer/API.py | 10 ++++++++- SoftLayer/auth.py | 17 +++++++++++++-- SoftLayer/tests/auth_tests.py | 3 ++- SoftLayer/tests/deprecated_tests.py | 33 +++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 SoftLayer/tests/deprecated_tests.py diff --git a/SoftLayer/API.py b/SoftLayer/API.py index f2d8bf75c..c306e1ae2 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -6,6 +6,7 @@ :license: MIT, see LICENSE for more details. """ import time +import warnings from SoftLayer import auth as slauth from SoftLayer import config @@ -183,7 +184,14 @@ def call(self, service, method, *args, **kwargs): } if self.auth: - options = self.auth.get_options(options) + if getattr(self.auth, "get_headers", None): + warnings.warn("auth.get_headers() is deprecation and will be " + "removed in the next major version", + DeprecationWarning) + headers.update(self.auth.get_headers()) + + if getattr(self.auth, "get_options", None): + options = self.auth.get_options(options) return transports.make_xml_rpc_api_call(uri, method, args, **options) diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index a0b1daf0b..931042e72 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -10,9 +10,22 @@ class AuthenticationBase(object): """A base authentication class intended to be overridden.""" + def get_options(self, options): - """Receives request options and returns request options.""" - raise NotImplementedError + """Receives request options and returns request options. + + :param options dict: dictionary of request options + + """ + return options + + def get_headers(self): + """Return a dictionary of headers to be inserted for authentication. + + .. deprecated:: 3.3.0 + Use :func:`get_options` instead. + """ + return {} class TokenAuthentication(AuthenticationBase): diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py index 3e1fe74ee..45405c7e8 100644 --- a/SoftLayer/tests/auth_tests.py +++ b/SoftLayer/tests/auth_tests.py @@ -11,7 +11,8 @@ class TestAuthenticationBase(testing.TestCase): def test_get_options(self): auth_base = auth.AuthenticationBase() - self.assertRaises(NotImplementedError, auth_base.get_options, {}) + self.assertEqual(auth_base.get_options({}), {}) + self.assertEqual(auth_base.get_headers(), {}) class TestBasicAuthentication(testing.TestCase): diff --git a/SoftLayer/tests/deprecated_tests.py b/SoftLayer/tests/deprecated_tests.py new file mode 100644 index 000000000..096b1c28d --- /dev/null +++ b/SoftLayer/tests/deprecated_tests.py @@ -0,0 +1,33 @@ +""" + SoftLayer.tests.depecated_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +import mock + +import SoftLayer +import SoftLayer.API +from SoftLayer import testing + + +class DeprecatedAuth(SoftLayer.AuthenticationBase): + """Auth that only implements get_headers().""" + + def get_headers(self): + return {'deprecated': 'header'} + + +class APIClient(testing.TestCase): + def set_up(self): + self.client = SoftLayer.Client(auth=DeprecatedAuth()) + + @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') + def test_simple_call(self, make_xml_rpc_api_call): + self.client['SERVICE'].METHOD() + make_xml_rpc_api_call.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, + headers={'deprecated': 'header'}, + proxy=mock.ANY, + timeout=mock.ANY, + http_headers=mock.ANY) From fc98a222070874871bf4889ec3f1687da6413fd8 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 19 Sep 2014 11:36:54 -0500 Subject: [PATCH 0079/2592] Adds better test/implementation for deprecated functionality --- SoftLayer/API.py | 10 +++++----- SoftLayer/tests/deprecated_tests.py | 24 +++++++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index c306e1ae2..e1956c92a 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -184,14 +184,14 @@ def call(self, service, method, *args, **kwargs): } if self.auth: - if getattr(self.auth, "get_headers", None): - warnings.warn("auth.get_headers() is deprecation and will be " + extra_headers = self.auth.get_headers() + if extra_headers: + warnings.warn("auth.get_headers() is deprecated and will be " "removed in the next major version", DeprecationWarning) - headers.update(self.auth.get_headers()) + headers.update(extra_headers) - if getattr(self.auth, "get_options", None): - options = self.auth.get_options(options) + options = self.auth.get_options(options) return transports.make_xml_rpc_api_call(uri, method, args, **options) diff --git a/SoftLayer/tests/deprecated_tests.py b/SoftLayer/tests/deprecated_tests.py index 096b1c28d..207d8e0ed 100644 --- a/SoftLayer/tests/deprecated_tests.py +++ b/SoftLayer/tests/deprecated_tests.py @@ -4,6 +4,8 @@ :license: MIT, see LICENSE for more details. """ +import warnings + import mock import SoftLayer @@ -24,10 +26,18 @@ def set_up(self): @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') def test_simple_call(self, make_xml_rpc_api_call): - self.client['SERVICE'].METHOD() - make_xml_rpc_api_call.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, - headers={'deprecated': 'header'}, - proxy=mock.ANY, - timeout=mock.ANY, - http_headers=mock.ANY) + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + self.client['SERVICE'].METHOD() + + make_xml_rpc_api_call.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, + headers={'deprecated': 'header'}, + proxy=mock.ANY, + timeout=mock.ANY, + http_headers=mock.ANY) + self.assertEqual(len(w), 1) + self.assertEqual(w[0].category, DeprecationWarning) + self.assertIn("deprecated", str(w[0].message)) From 8deeb4c160c358c273448493eb9400467c518955 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 22 Sep 2014 17:05:12 -0500 Subject: [PATCH 0080/2592] Adds generic SoftLayer request object This allows both transports to (mostly) use this one object to define the specifications of an API call. --- SoftLayer/API.py | 64 ++--- SoftLayer/auth.py | 16 +- SoftLayer/managers/metadata.py | 39 ++- SoftLayer/tests/api_tests.py | 177 ++++--------- SoftLayer/tests/auth_tests.py | 37 ++- SoftLayer/tests/deprecated_tests.py | 16 +- SoftLayer/tests/functional_tests.py | 10 +- SoftLayer/tests/managers/metadata_tests.py | 151 ++++++----- SoftLayer/tests/transport_tests.py | 286 +++++++++++++++++---- SoftLayer/transports.py | 170 +++++++++--- setup.cfg | 2 +- 11 files changed, 575 insertions(+), 393 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index e1956c92a..cab5e6921 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -145,24 +145,6 @@ def call(self, service, method, *args, **kwargs): if not service.startswith(self._prefix): service = self._prefix + service - headers = kwargs.get('headers', {}) - - if kwargs.get('id') is not None: - headers[service + 'InitParameters'] = {'id': kwargs.get('id')} - - if kwargs.get('mask') is not None: - headers.update(self.__format_object_mask(kwargs.get('mask'), - service)) - - if kwargs.get('filter') is not None: - headers['%sObjectFilter' % service] = kwargs.get('filter') - - if kwargs.get('limit'): - headers['resultLimit'] = { - 'limit': kwargs.get('limit'), - 'offset': kwargs.get('offset', 0), - } - http_headers = { 'User-Agent': self.user_agent or consts.USER_AGENT, 'Content-Type': 'application/xml', @@ -175,13 +157,19 @@ def call(self, service, method, *args, **kwargs): if kwargs.get('raw_headers'): http_headers.update(kwargs.get('raw_headers')) - uri = '/'.join([self.endpoint_url, service]) - options = { - 'headers': headers, - 'http_headers': http_headers, - 'timeout': self.timeout, - 'proxy': self.proxy, - } + request = transports.Request() + request.endpoint = self.endpoint_url + request.service = service + request.method = method + request.args = args + request.transport_headers = http_headers + request.timeout = self. timeout + request.proxy = self.proxy + request.identifier = kwargs.get('id') + request.mask = kwargs.get('mask') + request.filter = kwargs.get('filter') + request.limit = kwargs.get('limit') + request.offset = kwargs.get('offset') if self.auth: extra_headers = self.auth.get_headers() @@ -189,12 +177,11 @@ def call(self, service, method, *args, **kwargs): warnings.warn("auth.get_headers() is deprecated and will be " "removed in the next major version", DeprecationWarning) - headers.update(extra_headers) + request.headers.update(extra_headers) - options = self.auth.get_options(options) + request = self.auth.get_request(request) - return transports.make_xml_rpc_api_call(uri, method, args, - **options) + return transports.make_xml_rpc_api_call(request) __call__ = call @@ -249,25 +236,6 @@ def iter_call(self, service, method, if len(results) < chunk: break - def __format_object_mask(self, objectmask, service): - """Format new and old style object masks into proper headers. - - :param objectmask: a string- or dict-based object mask - :param service: a SoftLayer API service name - - """ - if isinstance(objectmask, dict): - mheader = '%sObjectMask' % service - else: - mheader = self._prefix + 'ObjectMask' - - objectmask = objectmask.strip() - if (not objectmask.startswith('mask') - and not objectmask.startswith('[')): - objectmask = "mask[%s]" % objectmask - - return {mheader: {'mask': objectmask}} - def __repr__(self): return "" % (self.endpoint_url, self.auth) diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 931042e72..c2a1435a4 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -11,13 +11,13 @@ class AuthenticationBase(object): """A base authentication class intended to be overridden.""" - def get_options(self, options): + def get_request(self, request): """Receives request options and returns request options. :param options dict: dictionary of request options """ - return options + return request def get_headers(self): """Return a dictionary of headers to be inserted for authentication. @@ -39,14 +39,14 @@ def __init__(self, user_id, auth_token): self.user_id = user_id self.auth_token = auth_token - def get_options(self, options): + def get_request(self, request): """Sets token-based auth headers.""" - options['headers']['authenticate'] = { + request.headers['authenticate'] = { 'complexType': 'PortalLoginToken', 'userId': self.user_id, 'authToken': self.auth_token, } - return options + return request def __repr__(self): return "" % (self.user_id, self.auth_token) @@ -62,13 +62,13 @@ def __init__(self, username, api_key): self.username = username self.api_key = api_key - def get_options(self, options): + def get_request(self, request): """Sets token-based auth headers.""" - options['headers']['authenticate'] = { + request.headers['authenticate'] = { 'username': self.username, 'apiKey': self.api_key, } - return options + return request def __repr__(self): return "" % (self.username) diff --git a/SoftLayer/managers/metadata.py b/SoftLayer/managers/metadata.py index dda795812..6d6b47b2a 100644 --- a/SoftLayer/managers/metadata.py +++ b/SoftLayer/managers/metadata.py @@ -58,22 +58,6 @@ def __init__(self, client=None, timeout=5): self.timeout = timeout self.client = client - def make_request(self, path): - """ Make a request against the metadata service - - :param string path: path to the specific metadata resource - """ - url = '/'.join([self.url, 'SoftLayer_Resource_Metadata', path]) - try: - return transports.make_rest_api_call( - 'GET', url, - http_headers={'User-Agent': consts.USER_AGENT}, - timeout=self.timeout) - except exceptions.SoftLayerAPIError as ex: - if ex.faultCode == 404: - return None - raise ex - def get(self, name, param=None): """ Retreive a metadata attribute @@ -85,19 +69,30 @@ def get(self, name, param=None): raise exceptions.SoftLayerError('Unknown metadata attribute.') call_details = self.attribs[name] - extension = '.json' + extension = 'json' if self.attribs[name]['call'] == 'UserMetadata': - extension = '.txt' + extension = 'txt' if call_details.get('param_req'): if not param: raise exceptions.SoftLayerError( 'Parameter required to get this attribute.') - path = "%s/%s%s" % (self.attribs[name]['call'], param, extension) - else: - path = "%s%s" % (self.attribs[name]['call'], extension) - return self.make_request(path) + request = transports.Request() + request.endpoint = self.url + request.service = 'SoftLayer_Resource_Metadata' + request.method = self.attribs[name]['call'] + request.transport_headers = {'User-Agent': consts.USER_AGENT} + request.timeout = self.timeout + request.identifier = param + + try: + return transports.make_rest_api_call(request, + extension=extension) + except exceptions.SoftLayerAPIError as ex: + if ex.faultCode == 404: + return None + raise ex def _get_network(self, kind, router=True, vlans=True, vlan_ids=True): """ Wrapper for getting details about networks diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 8260f4e62..c64c33d4e 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -8,9 +8,11 @@ import SoftLayer import SoftLayer.API -from SoftLayer import consts from SoftLayer import testing +TEST_AUTH_HEADERS = { + 'authenticate': {'apiKey': 'issurelywrong', 'username': 'doesnotexist'}} + class Inititialization(testing.TestCase): def test_init(self): @@ -79,109 +81,49 @@ def set_up(self): @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') def test_simple_call(self, make_xml_rpc_api_call): - self.client['SERVICE'].METHOD() - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers={ - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}}, - proxy=None, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, compress', - }) + make_xml_rpc_api_call.return_value = {"test": "result"} + resp = self.client['SERVICE'].METHOD() + + self.assertEqual(resp, {"test": "result"}) + + (request,), kwargs = make_xml_rpc_api_call.call_args + self.assertEqual(request.endpoint, self.client.endpoint_url) + self.assertEqual(request.service, 'SoftLayer_SERVICE') + self.assertEqual(request.method, 'METHOD') + self.assertEqual(request.mask, None) + self.assertEqual(request.filter, None) + self.assertEqual(request.identifier, None) + self.assertEqual(request.args, tuple()) + self.assertEqual(request.limit, None) + self.assertEqual(request.offset, None) + self.assertEqual(request.headers, TEST_AUTH_HEADERS) @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') def test_complex(self, make_xml_rpc_api_call): - self.client['SERVICE'].METHOD( + make_xml_rpc_api_call.return_value = {"test": "result"} + resp = self.client['SERVICE'].METHOD( 1234, id=5678, mask={'object': {'attribute': ''}}, raw_headers={'RAW': 'HEADER'}, filter={ - 'TYPE': {'obj': {'attribute': {'operation': '^= prefix'}}}}, + 'TYPE': {'attribute': {'operation': '^= prefix'}}}, limit=9, offset=10) - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (1234, ), - headers={ - 'SoftLayer_SERVICEObjectMask': { - 'mask': {'object': {'attribute': ''}}}, - 'SoftLayer_SERVICEObjectFilter': { - 'TYPE': { - 'obj': {'attribute': {'operation': '^= prefix'}}}}, - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, - 'SoftLayer_SERVICEInitParameters': {'id': 5678}, - 'resultLimit': {'limit': 9, 'offset': 10}}, - proxy=None, - timeout=None, - http_headers={ - 'RAW': 'HEADER', - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, compress', - }) - - @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') - def test_mask_call_v2(self, make_xml_rpc_api_call): - self.client['SERVICE'].METHOD( - mask="mask[something[nested]]") - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers={ - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, - 'SoftLayer_ObjectMask': {'mask': 'mask[something[nested]]'}}, - proxy=None, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, compress', - }) - - @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') - def test_mask_call_v2_dot(self, make_xml_rpc_api_call): - self.client['SERVICE'].METHOD( - mask="mask.something.nested") - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers={ - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, - 'SoftLayer_ObjectMask': {'mask': 'mask.something.nested'}}, - proxy=None, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, compress', - }) - - @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') - def test_mask_call_no_mask_prefix(self, make_xml_rpc_api_call): - self.client['SERVICE'].METHOD(mask="something.nested") - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers={ - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, - 'SoftLayer_ObjectMask': {'mask': 'mask[something.nested]'}}, - proxy=None, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, compress', - }) + self.assertEqual(resp, {"test": "result"}) + + (request,), kwargs = make_xml_rpc_api_call.call_args + self.assertEqual(request.endpoint, self.client.endpoint_url) + self.assertEqual(request.service, 'SoftLayer_SERVICE') + self.assertEqual(request.method, 'METHOD') + self.assertEqual(request.mask, {'object': {'attribute': ''}}) + self.assertEqual(request.filter, + {'TYPE': {'attribute': {'operation': '^= prefix'}}}) + self.assertEqual(request.identifier, 5678) + self.assertEqual(request.args, (1234,)) + self.assertEqual(request.limit, 9) + self.assertEqual(request.offset, 10) + self.assertEqual(request.headers, TEST_AUTH_HEADERS) @mock.patch('SoftLayer.API.Client.iter_call') def test_iterate(self, _iter_call): @@ -250,8 +192,8 @@ def test_iter_call(self, _call): # Chunk size of 0 is invalid self.assertRaises( AttributeError, - lambda: list(self.client.iter_call( - 'SERVICE', 'METHOD', iter=True, chunk=0))) + lambda: list(self.client.iter_call('SERVICE', 'METHOD', + iter=True, chunk=0))) def test_call_invalid_arguments(self): self.assertRaises( @@ -261,30 +203,19 @@ def test_call_invalid_arguments(self): @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') def test_call_compression_disabled(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD(compress=False) - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers=mock.ANY, - proxy=None, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - }) + + (request,), kwargs = make_xml_rpc_api_call.call_args + self.assertNotIn('Accept-Encoding', request.transport_headers) + self.assertNotIn('Accept', request.transport_headers) @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') def test_call_compression_enabled(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD(compress=True) - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers=mock.ANY, - proxy=None, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, compress', - }) + + (request,), kwargs = make_xml_rpc_api_call.call_args + headers = request.transport_headers + self.assertEqual(headers['Accept-Encoding'], 'gzip, deflate, compress') + self.assertEqual(headers['Accept'], '*/*') @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') def test_call_compression_override(self, make_xml_rpc_api_call): @@ -292,16 +223,10 @@ def test_call_compression_override(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD( compress=False, raw_headers={'Accept-Encoding': 'gzip'}) - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers=mock.ANY, - proxy=None, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': consts.USER_AGENT, - 'Accept-Encoding': 'gzip', - }) + + (request,), kwargs = make_xml_rpc_api_call.call_args + self.assertEqual(request.transport_headers['Accept-Encoding'], 'gzip') + self.assertNotIn('Accept', request.transport_headers) class APITimedClient(testing.TestCase): diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py index 45405c7e8..240af3a99 100644 --- a/SoftLayer/tests/auth_tests.py +++ b/SoftLayer/tests/auth_tests.py @@ -6,12 +6,13 @@ """ from SoftLayer import auth from SoftLayer import testing +from SoftLayer import transports class TestAuthenticationBase(testing.TestCase): - def test_get_options(self): + def test_get_request(self): auth_base = auth.AuthenticationBase() - self.assertEqual(auth_base.get_options({}), {}) + self.assertEqual(auth_base.get_request({}), {}) self.assertEqual(auth_base.get_headers(), {}) @@ -23,14 +24,13 @@ def test_attribs(self): self.assertEqual(self.auth.username, 'USERNAME') self.assertEqual(self.auth.api_key, 'APIKEY') - def test_get_options(self): - headers = {'headers': {}} - self.assertEqual(self.auth.get_options(headers), { - 'headers': { - 'authenticate': { - 'username': 'USERNAME', - 'apiKey': 'APIKEY', - } + def test_get_request(self): + req = transports.Request() + authed_req = self.auth.get_request(req) + self.assertEqual(authed_req.headers, { + 'authenticate': { + 'username': 'USERNAME', + 'apiKey': 'APIKEY', } }) @@ -48,15 +48,14 @@ def test_attribs(self): self.assertEqual(self.auth.user_id, 12345) self.assertEqual(self.auth.auth_token, 'TOKEN') - def test_get_options(self): - headers = {'headers': {}} - self.assertEqual(self.auth.get_options(headers), { - 'headers': { - 'authenticate': { - 'complexType': 'PortalLoginToken', - 'userId': 12345, - 'authToken': 'TOKEN', - } + def test_get_request(self): + req = transports.Request() + authed_req = self.auth.get_request(req) + self.assertEqual(authed_req.headers, { + 'authenticate': { + 'complexType': 'PortalLoginToken', + 'userId': 12345, + 'authToken': 'TOKEN', } }) diff --git a/SoftLayer/tests/deprecated_tests.py b/SoftLayer/tests/deprecated_tests.py index 207d8e0ed..f4918c91f 100644 --- a/SoftLayer/tests/deprecated_tests.py +++ b/SoftLayer/tests/deprecated_tests.py @@ -30,14 +30,14 @@ def test_simple_call(self, make_xml_rpc_api_call): # Cause all warnings to always be triggered. warnings.simplefilter("always") - self.client['SERVICE'].METHOD() - - make_xml_rpc_api_call.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, - headers={'deprecated': 'header'}, - proxy=mock.ANY, - timeout=mock.ANY, - http_headers=mock.ANY) + make_xml_rpc_api_call.return_value = {"test": "result"} + resp = self.client['SERVICE'].METHOD() + + self.assertEqual(resp, {"test": "result"}) + + (request,), kwargs = make_xml_rpc_api_call.call_args + self.assertEqual(request.headers, {'deprecated': 'header'}) + self.assertEqual(len(w), 1) self.assertEqual(w[0].category, DeprecationWarning) self.assertIn("deprecated", str(w[0].message)) diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py index d121f9958..18408254c 100644 --- a/SoftLayer/tests/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -8,6 +8,7 @@ import SoftLayer from SoftLayer import testing +from SoftLayer import transports def get_creds(): @@ -34,9 +35,14 @@ def test_failed_auth(self): def test_no_hostname(self): try: + request = transports.Request() + request.endpoint = 'http://notvalidsoftlayer.com' + request.service = 'SoftLayer_Account' + request.method = 'getObject' + request.id = 1234 + # This test will fail if 'notvalidsoftlayer.com' becomes a thing - SoftLayer.transports.make_xml_rpc_api_call( - 'http://notvalidsoftlayer.com', 'getObject') + transports.make_xml_rpc_api_call(request) except SoftLayer.SoftLayerAPIError as ex: self.assertIn('not known', str(ex)) self.assertIn('not known', ex.faultString) diff --git a/SoftLayer/tests/managers/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py index 363982c26..93681c052 100644 --- a/SoftLayer/tests/managers/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -15,49 +15,100 @@ class MetadataTests(testing.TestCase): def set_up(self): self.metadata = SoftLayer.MetadataManager() - self.make_request = mock.MagicMock() - self.metadata.make_request = self.make_request - - def test_no_param(self): - self.make_request.return_value = 'dal01' - r = self.metadata.get('datacenter') - self.make_request.assert_called_with("Datacenter.json") - self.assertEqual('dal01', r) - - def test_w_param(self): - self.make_request.return_value = [123] - r = self.metadata.get('vlans', '1:2:3:4:5') - self.make_request.assert_called_with("Vlans/1:2:3:4:5.json") - self.assertEqual([123], r) - - def test_user_data(self): - self.make_request.return_value = 'user_data' - r = self.metadata.get('user_data') - self.make_request.assert_called_with("UserMetadata.txt") - self.assertEqual('user_data', r) - - def test_return_none(self): - self.make_request.return_value = None - r = self.metadata.get('datacenter') - self.make_request.assert_called_with("Datacenter.json") - self.assertEqual(None, r) - - def test_w_param_error(self): + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_get(self, make_request): + make_request.return_value = 'dal01' + resp = self.metadata.get('datacenter') + + self.assertEqual('dal01', resp) + + (request, ), kwargs = make_request.call_args + + self.assertEqual(request.endpoint, self.metadata.url) + self.assertEqual(request.service, 'SoftLayer_Resource_Metadata') + self.assertEqual(request.method, 'Datacenter') + self.assertEqual(request.transport_headers, + {'User-Agent': consts.USER_AGENT}) + self.assertEqual(request.timeout, self.metadata.timeout) + self.assertEqual(request.identifier, None) + self.assertEqual(kwargs['extension'], 'json') + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_no_param(self, make_request): + make_request.return_value = 'dal01' + + resp = self.metadata.get('datacenter') + + self.assertEqual('dal01', resp) + (request, ), kwargs = make_request.call_args + self.assertEqual(request.method, 'Datacenter') + self.assertEqual(request.identifier, None) + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_w_param(self, make_request): + make_request.return_value = [123] + resp = self.metadata.get('vlans', '1:2:3:4:5') + + self.assertEqual([123], resp) + (request, ), kwargs = make_request.call_args + self.assertEqual(request.method, 'Vlans') + self.assertEqual(request.identifier, '1:2:3:4:5') + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_user_data(self, make_request): + make_request.return_value = 'user_data' + resp = self.metadata.get('user_data') + + self.assertEqual('user_data', resp) + (request, ), kwargs = make_request.call_args + self.assertEqual(request.method, 'UserMetadata') + self.assertEqual(request.identifier, None) + self.assertEqual(kwargs['extension'], 'txt') + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_return_none(self, make_request): + make_request.return_value = None + resp = self.metadata.get('datacenter') + + self.assertEqual(None, resp) + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_404(self, make_request): + make_request.side_effect = SoftLayer.SoftLayerAPIError(404, + 'Not Found') + resp = self.metadata.get('user_data') + + self.assertEqual(None, resp) + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_error(self, make_request): + exception = SoftLayer.SoftLayerAPIError(500, 'Error') + make_request.side_effect = exception + + self.assertRaises(SoftLayer.SoftLayerAPIError, + self.metadata.get, 'user_data') + + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_w_param_error(self, make_request): self.assertRaises(SoftLayer.SoftLayerError, self.metadata.get, 'vlans') - def test_not_exists(self): + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_not_exists(self, make_request): self.assertRaises(SoftLayer.SoftLayerError, self.metadata.get, 'something') - def test_networks_not_exist(self): - self.make_request.return_value = [] + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_networks_not_exist(self, make_request): + make_request.return_value = [] r = self.metadata.public_network() self.assertEqual({'mac_addresses': []}, r) - def test_networks(self): + @mock.patch('SoftLayer.transports.make_rest_api_call') + def test_networks(self, make_request): resp = ['list', 'of', 'stuff'] - self.make_request.return_value = resp + make_request.return_value = resp r = self.metadata.public_network() self.assertEqual({ 'vlan_ids': resp, @@ -73,37 +124,3 @@ def test_networks(self): 'vlans': resp, 'mac_addresses': resp }, r) - - -class MetadataTestsMakeRequest(testing.TestCase): - - def set_up(self): - self.metadata = SoftLayer.MetadataManager() - self.url = '/'.join([ - consts.API_PRIVATE_ENDPOINT_REST.rstrip('/'), - 'SoftLayer_Resource_Metadata', - 'something.json']) - - @mock.patch('SoftLayer.transports.make_rest_api_call') - def test_basic(self, make_api_call): - r = self.metadata.make_request('something.json') - make_api_call.assert_called_with( - 'GET', self.url, - timeout=5, - http_headers={'User-Agent': consts.USER_AGENT}) - self.assertEqual(make_api_call(), r) - - @mock.patch('SoftLayer.transports.make_rest_api_call') - def test_raise_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError( - 'faultCode', 'faultString') - self.assertRaises( - SoftLayer.SoftLayerAPIError, - self.metadata.make_request, 'something.json') - - @mock.patch('SoftLayer.transports.make_rest_api_call') - def test_raise_404_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError(404, - 'faultString') - r = self.metadata.make_request('something.json') - self.assertEqual(r, None) diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index e65ca6595..8d02b0236 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -45,14 +45,15 @@ def test_call(self, request): ''' - resp = transports.make_xml_rpc_api_call( - 'http://something.com/path/to/resource', 'getObject') - args = request.call_args - self.assertIsNotNone(args) - args, kwargs = args + + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'getObject' + resp = transports.make_xml_rpc_api_call(req) request.assert_called_with('POST', - 'http://something.com/path/to/resource', + 'http://something.com/SoftLayer_Service', headers=None, proxies=None, data=data, @@ -62,20 +63,26 @@ def test_call(self, request): self.assertEqual(resp, []) def test_proxy_without_protocol(self): - self.assertRaises( - SoftLayer.TransportError, # NOQA - transports.make_xml_rpc_api_call, - 'http://something.com/path/to/resource', - 'getObject', - 'localhost:3128') + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'Resource' + req.proxy = 'localhost:3128' + + self.assertRaises(SoftLayer.TransportError, + transports.make_xml_rpc_api_call, req) @mock.patch('requests.request') def test_valid_proxy(self, request): request.return_value = self.response - transports.make_xml_rpc_api_call( - 'http://something.com/path/to/resource', - 'getObject', - proxy='http://localhost:3128') + + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'Resource' + req.proxy = 'http://localhost:3128' + transports.make_xml_rpc_api_call(req) + request.assert_called_with( 'POST', mock.ANY, @@ -87,17 +94,163 @@ def test_valid_proxy(self, request): cert=None, verify=True) + @mock.patch('requests.request') + def test_identifier(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.identifier = 1234 + transports.make_xml_rpc_api_call(req) + + args, kwargs = request.call_args + self.assertIn( + """ +id +1234 +""", kwargs['data']) + + @mock.patch('requests.request') + def test_filter(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.filter = {'TYPE': {'attribute': {'operation': '^= prefix'}}} + transports.make_xml_rpc_api_call(req) + + args, kwargs = request.call_args + self.assertIn( + """ +operation +^= prefix +""", kwargs['data']) + + @mock.patch('requests.request') + def test_limit_offset(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.limit = 10 + transports.make_xml_rpc_api_call(req) + + args, kwargs = request.call_args + self.assertIn(""" +resultLimit + +""", kwargs['data']) + self.assertIn("""limit +10 +""", kwargs['data']) + + @mock.patch('requests.request') + def test_old_mask(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.mask = {"something": "nested"} + transports.make_xml_rpc_api_call(req) + + args, kwargs = request.call_args + self.assertIn(""" +mask + + +something +nested + + +""", kwargs['data']) + + @mock.patch('requests.request') + def test_mask_call_no_mask_prefix(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.mask = "something.nested" + transports.make_xml_rpc_api_call(req) + + args, kwargs = request.call_args + self.assertIn( + "mask[something.nested]", + kwargs['data']) + + @mock.patch('requests.request') + def test_mask_call_v2(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.mask = "mask[something[nested]]" + transports.make_xml_rpc_api_call(req) + + args, kwargs = request.call_args + self.assertIn( + "mask[something[nested]]", + kwargs['data']) + + @mock.patch('requests.request') + def test_mask_call_v2_dot(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.mask = "mask.something.nested" + transports.make_xml_rpc_api_call(req) + + args, kwargs = request.call_args + self.assertIn("mask.something.nested", + kwargs['data']) + + @mock.patch('requests.request') + def test_request_exception(self, request): + # Test Text Error + e = requests.HTTPError('error') + e.response = mock.MagicMock() + e.response.status_code = 404 + e.response.content = 'Error Code' + request().raise_for_status.side_effect = e + + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'getObject' + + self.assertRaises(SoftLayer.TransportError, + transports.make_xml_rpc_api_call, req) + class TestRestAPICall(testing.TestCase): - @mock.patch('SoftLayer.transports.requests.request') + @mock.patch('requests.request') def test_json(self, request): request().content = '{}' - resp = transports.make_rest_api_call( - 'GET', 'http://something.com/path/to/resource.json') + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'Resource' + + resp = transports.make_rest_api_call(req, extension='json') self.assertEqual(resp, {}) request.assert_called_with( - 'GET', 'http://something.com/path/to/resource.json', + 'GET', 'http://something.com/SoftLayer_Service/Resource.json', headers=None, proxies=None, timeout=None) @@ -114,39 +267,71 @@ def test_json(self, request): self.assertRaises( SoftLayer.SoftLayerAPIError, - transports.make_rest_api_call, - 'GET', - 'http://something.com/path/to/resource.json') + transports.make_rest_api_call, req, extension='json') def test_proxy_without_protocol(self): - self.assertRaises( - SoftLayer.TransportError, - transports.make_rest_api_call, - 'GET' - 'http://something.com/path/to/resource.txt', - 'localhost:3128') + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'Resource' + req.proxy = 'localhost:3128' + + self.assertRaises(SoftLayer.TransportError, + transports.make_rest_api_call, req) - @mock.patch('SoftLayer.transports.requests.request') + @mock.patch('requests.request') def test_valid_proxy(self, request): - transports.make_rest_api_call( - 'GET', - 'http://something.com/path/to/resource.txt', - proxy='http://localhost:3128') + request().content = '{}' + + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'Resource' + req.proxy = 'http://localhost:3128' + + transports.make_rest_api_call(req) request.assert_called_with( - 'GET', 'http://something.com/path/to/resource.txt', - headers=mock.ANY, + 'GET', 'http://something.com/SoftLayer_Service/Resource.json', proxies={'https': 'http://localhost:3128', 'http': 'http://localhost:3128'}, + timeout=mock.ANY, + headers=mock.ANY) + + @mock.patch('requests.request') + def test_with_id(self, request): + request().content = '{}' + + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'getObject' + req.identifier = 2 + + resp = transports.make_rest_api_call(req) + + self.assertEqual(resp, {}) + request.assert_called_with( + 'GET', + 'http://something.com/SoftLayer_Service/getObject/2.json', + headers=None, + proxies=None, timeout=None) - @mock.patch('SoftLayer.transports.requests.request') + @mock.patch('requests.request') def test_text(self, request): + request().content = 'content' request().text = 'content' - resp = transports.make_rest_api_call( - 'GET', 'http://something.com/path/to/resource.txt') + + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'Resource' + + resp = transports.make_rest_api_call(req, extension='txt') self.assertEqual(resp, 'content') request.assert_called_with( - 'GET', 'http://something.com/path/to/resource.txt', + 'GET', + 'http://something.com/SoftLayer_Service/Resource.txt', headers=None, proxies=None, timeout=None) @@ -158,13 +343,10 @@ def test_text(self, request): e.response.content = 'Error Code' request().raise_for_status.side_effect = e - self.assertRaises( - SoftLayer.SoftLayerAPIError, - transports.make_rest_api_call, - 'GET', - 'http://something.com/path/to/resource.txt') + self.assertRaises(SoftLayer.SoftLayerAPIError, + transports.make_rest_api_call, req, extension='txt') - @mock.patch('SoftLayer.transports.requests.request') + @mock.patch('requests.request') def test_unknown_error(self, request): e = requests.RequestException('error') e.response = mock.MagicMock() @@ -172,8 +354,10 @@ def test_unknown_error(self, request): e.response.content = 'Error Code' request().raise_for_status.side_effect = e - self.assertRaises( - SoftLayer.TransportError, - transports.make_rest_api_call, - 'GET', - 'http://something.com/path/to/resource.txt') + req = transports.Request() + req.endpoint = 'http://something.com' + req.service = 'SoftLayer_Service' + req.method = 'getObject' + + self.assertRaises(SoftLayer.TransportError, + transports.make_rest_api_call, req) diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 332ae9a98..76a0ea6f2 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -14,49 +14,104 @@ import requests LOGGER = logging.getLogger(__name__) +# transports.Request does have a lot of instance attributes. :( +# pylint: disable=too-many-instance-attributes -def _proxies_dict(proxy): - """Makes a dict appropriate to pass to requests.""" - if not proxy: - return None - return {'http': proxy, 'https': proxy} +class Request(object): + """Transport request object.""" + + def __init__(self): + #: The SoftLayer endpoint address. + self.endpoint = None + + #: Service name. + self.service = None + + #: Method name. + self.method = None + + #: RPC Arguments. + self.args = [] + + #: Transport headers. + self.headers = {} + + #: Transport headers. + self.transport_headers = None + #: Integer timeout. + self.timeout = None -def make_xml_rpc_api_call(url, method, args=None, headers=None, - http_headers=None, timeout=None, proxy=None, - verify=True, cert=None): + #: URL to proxy to. + self.proxy = None + + #: Verify HTTPS Certificate. + self.verify = True + + #: Client certificate path. + self.cert = None + + #: InitParameter/identifier of an object. + self.identifier = None + + #: SoftLayer mask (dict or string). + self.mask = None + + #: SoftLayer Filter (dict). + self.filter = None + + #: Integer result limit. + self.limit = None + + #: Integer result offset. + self.offset = None + + +def make_xml_rpc_api_call(request): """Makes a SoftLayer API call against the XML-RPC endpoint. - :param string uri: endpoint URL - :param string method: method to call E.G.: 'getObject' - :param dict headers: XML-RPC headers to use for the request - :param dict http_headers: HTTP headers to use for the request - :param int timeout: number of seconds to use as a timeout - :param bool verify: verify SSL cert - :param cert: client certificate path + :param request request: Request object """ - if args is None: - args = tuple() try: - largs = list(args) - largs.insert(0, {'headers': headers}) + largs = list(request.args) + + headers = request.headers + + if request.identifier is not None: + header_name = request.service + 'InitParameters' + headers[header_name] = {'id': request.identifier} + if request.mask is not None: + headers.update(__format_object_mask(request.mask, request.service)) + + if request.filter is not None: + headers['%sObjectFilter' % request.service] = request.filter + + if request.limit: + headers['resultLimit'] = { + 'limit': request.limit, + 'offset': request.offset or 0, + } + + largs.insert(0, {'headers': headers or None}) + + url = '/'.join([request.endpoint, request.service]) payload = utils.xmlrpc_client.dumps(tuple(largs), - methodname=method, + methodname=request.method, allow_none=True) LOGGER.debug("=== REQUEST ===") LOGGER.info('POST %s', url) - LOGGER.debug(http_headers) + LOGGER.debug(request.transport_headers) LOGGER.debug(payload) response = requests.request('POST', url, data=payload, - headers=http_headers, - timeout=timeout, - verify=verify, - cert=cert, - proxies=_proxies_dict(proxy)) + headers=request.transport_headers, + timeout=request.timeout, + verify=request.verify, + cert=request.cert, + proxies=_proxies_dict(request.proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(response.headers) LOGGER.debug(response.content) @@ -86,38 +141,71 @@ def make_xml_rpc_api_call(url, method, args=None, headers=None, raise exceptions.TransportError(0, str(ex)) -def make_rest_api_call(method, url, - http_headers=None, timeout=None, proxy=None): +def make_rest_api_call(request, extension='json'): """Makes a SoftLayer API call against the REST endpoint. - :param string method: HTTP method: GET, POST, PUT, DELETE - :param string url: endpoint URL - :param dict http_headers: HTTP headers to use for the request - :param int timeout: number of seconds to use as a timeout + This currently only works with GET requests + + :param request request: Request object """ + url_parts = [request.endpoint, + request.service, + request.method] + if request.identifier is not None: + url_parts.append(str(request.identifier)) + + url = '%s.%s' % ('/'.join(url_parts), extension) + LOGGER.debug("=== REQUEST ===") - LOGGER.info('%s %s', method, url) - LOGGER.debug(http_headers) + LOGGER.info('%s %s', request.method, url) + LOGGER.debug(request.transport_headers) try: - resp = requests.request(method, url, - headers=http_headers, - timeout=timeout, - proxies=_proxies_dict(proxy)) + resp = requests.request('GET', url, + headers=request.transport_headers, + timeout=request.timeout, + proxies=_proxies_dict(request.proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(resp.headers) LOGGER.debug(resp.content) resp.raise_for_status() - if url.endswith('.json'): + if extension == 'json': return json.loads(resp.content) else: return resp.text except requests.HTTPError as ex: - if url.endswith('.json'): + if extension == 'json': content = json.loads(ex.response.content) raise exceptions.SoftLayerAPIError(ex.response.status_code, content['error']) else: raise exceptions.SoftLayerAPIError(ex.response.status_code, - ex.response.text) + ex.response.content) except requests.RequestException as ex: raise exceptions.TransportError(0, str(ex)) + + +def _proxies_dict(proxy): + """Makes a dict appropriate to pass to requests.""" + if not proxy: + return None + return {'http': proxy, 'https': proxy} + + +def __format_object_mask(objectmask, service): + """Format new and old style object masks into proper headers. + + :param objectmask: a string- or dict-based object mask + :param service: a SoftLayer API service name + + """ + if isinstance(objectmask, dict): + mheader = '%sObjectMask' % service + else: + mheader = 'SoftLayer_ObjectMask' + + objectmask = objectmask.strip() + if (not objectmask.startswith('mask') + and not objectmask.startswith('[')): + objectmask = "mask[%s]" % objectmask + + return {mheader: {'mask': objectmask}} diff --git a/setup.cfg b/setup.cfg index d53bef49e..3d9f45499 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ verbosity=2 detailed-errors=1 with-coverage=1 -cover-min-percentage=74 +cover-min-percentage=75 cover-erase=true cover-package=SoftLayer cover-html=1 From e3045fa10bc71fa29ca9fc3f889feeed06b45b32 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 22 Sep 2014 17:11:33 -0500 Subject: [PATCH 0081/2592] Removes ability to specify which rest file extension to use --- SoftLayer/managers/metadata.py | 6 +--- SoftLayer/tests/managers/metadata_tests.py | 2 -- SoftLayer/tests/transport_tests.py | 34 ++-------------------- SoftLayer/transports.py | 19 ++++-------- 4 files changed, 9 insertions(+), 52 deletions(-) diff --git a/SoftLayer/managers/metadata.py b/SoftLayer/managers/metadata.py index 6d6b47b2a..074415997 100644 --- a/SoftLayer/managers/metadata.py +++ b/SoftLayer/managers/metadata.py @@ -69,9 +69,6 @@ def get(self, name, param=None): raise exceptions.SoftLayerError('Unknown metadata attribute.') call_details = self.attribs[name] - extension = 'json' - if self.attribs[name]['call'] == 'UserMetadata': - extension = 'txt' if call_details.get('param_req'): if not param: @@ -87,8 +84,7 @@ def get(self, name, param=None): request.identifier = param try: - return transports.make_rest_api_call(request, - extension=extension) + return transports.make_rest_api_call(request) except exceptions.SoftLayerAPIError as ex: if ex.faultCode == 404: return None diff --git a/SoftLayer/tests/managers/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py index 93681c052..592924e15 100644 --- a/SoftLayer/tests/managers/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -32,7 +32,6 @@ def test_get(self, make_request): {'User-Agent': consts.USER_AGENT}) self.assertEqual(request.timeout, self.metadata.timeout) self.assertEqual(request.identifier, None) - self.assertEqual(kwargs['extension'], 'json') @mock.patch('SoftLayer.transports.make_rest_api_call') def test_no_param(self, make_request): @@ -64,7 +63,6 @@ def test_user_data(self, make_request): (request, ), kwargs = make_request.call_args self.assertEqual(request.method, 'UserMetadata') self.assertEqual(request.identifier, None) - self.assertEqual(kwargs['extension'], 'txt') @mock.patch('SoftLayer.transports.make_rest_api_call') def test_return_none(self, make_request): diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 8d02b0236..972653a64 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -247,7 +247,7 @@ def test_json(self, request): req.service = 'SoftLayer_Service' req.method = 'Resource' - resp = transports.make_rest_api_call(req, extension='json') + resp = transports.make_rest_api_call(req) self.assertEqual(resp, {}) request.assert_called_with( 'GET', 'http://something.com/SoftLayer_Service/Resource.json', @@ -266,8 +266,7 @@ def test_json(self, request): request().raise_for_status.side_effect = e self.assertRaises( - SoftLayer.SoftLayerAPIError, - transports.make_rest_api_call, req, extension='json') + SoftLayer.SoftLayerAPIError, transports.make_rest_api_call, req) def test_proxy_without_protocol(self): req = transports.Request() @@ -317,35 +316,6 @@ def test_with_id(self, request): proxies=None, timeout=None) - @mock.patch('requests.request') - def test_text(self, request): - request().content = 'content' - request().text = 'content' - - req = transports.Request() - req.endpoint = 'http://something.com' - req.service = 'SoftLayer_Service' - req.method = 'Resource' - - resp = transports.make_rest_api_call(req, extension='txt') - self.assertEqual(resp, 'content') - request.assert_called_with( - 'GET', - 'http://something.com/SoftLayer_Service/Resource.txt', - headers=None, - proxies=None, - timeout=None) - - # Test Text Error - e = requests.HTTPError('error') - e.response = mock.MagicMock() - e.response.status_code = 404 - e.response.content = 'Error Code' - request().raise_for_status.side_effect = e - - self.assertRaises(SoftLayer.SoftLayerAPIError, - transports.make_rest_api_call, req, extension='txt') - @mock.patch('requests.request') def test_unknown_error(self, request): e = requests.RequestException('error') diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 76a0ea6f2..4b23ddc4b 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -141,7 +141,7 @@ def make_xml_rpc_api_call(request): raise exceptions.TransportError(0, str(ex)) -def make_rest_api_call(request, extension='json'): +def make_rest_api_call(request): """Makes a SoftLayer API call against the REST endpoint. This currently only works with GET requests @@ -154,7 +154,7 @@ def make_rest_api_call(request, extension='json'): if request.identifier is not None: url_parts.append(str(request.identifier)) - url = '%s.%s' % ('/'.join(url_parts), extension) + url = '%s.%s' % ('/'.join(url_parts), 'json') LOGGER.debug("=== REQUEST ===") LOGGER.info('%s %s', request.method, url) @@ -168,18 +168,11 @@ def make_rest_api_call(request, extension='json'): LOGGER.debug(resp.headers) LOGGER.debug(resp.content) resp.raise_for_status() - if extension == 'json': - return json.loads(resp.content) - else: - return resp.text + return json.loads(resp.content) except requests.HTTPError as ex: - if extension == 'json': - content = json.loads(ex.response.content) - raise exceptions.SoftLayerAPIError(ex.response.status_code, - content['error']) - else: - raise exceptions.SoftLayerAPIError(ex.response.status_code, - ex.response.content) + content = json.loads(ex.response.content) + raise exceptions.SoftLayerAPIError(ex.response.status_code, + content['error']) except requests.RequestException as ex: raise exceptions.TransportError(0, str(ex)) From 36aa8eb3e04256b7716cc57b069bf72d87803426 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 22 Sep 2014 17:30:04 -0500 Subject: [PATCH 0082/2592] Fixes user-metadata bug/clearifies debugging output --- SoftLayer/CLI/modules/metadata.py | 2 +- SoftLayer/transports.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index 07f6c12f3..4bf89d9b4 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -181,7 +181,7 @@ def _execute(self, _): separator=',') -class UserMetadata(environment.CLIRunnable): +class UserMetadata(MetaRunnable): """ usage: sl metadata user_data [options] diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 4b23ddc4b..05c02f3b2 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -157,7 +157,7 @@ def make_rest_api_call(request): url = '%s.%s' % ('/'.join(url_parts), 'json') LOGGER.debug("=== REQUEST ===") - LOGGER.info('%s %s', request.method, url) + LOGGER.info(url) LOGGER.debug(request.transport_headers) try: resp = requests.request('GET', url, From c987c8309628caaa517fd74f15483f8863d3cd75 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 22 Sep 2014 17:50:52 -0500 Subject: [PATCH 0083/2592] Don't set headers to None when they're empty --- SoftLayer/tests/transport_tests.py | 4 +++- SoftLayer/transports.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 972653a64..4afec4f1c 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -39,7 +39,9 @@ def test_call(self, request): headers - + + + diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 05c02f3b2..2e9ff64c5 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -94,7 +94,7 @@ def make_xml_rpc_api_call(request): 'offset': request.offset or 0, } - largs.insert(0, {'headers': headers or None}) + largs.insert(0, {'headers': headers}) url = '/'.join([request.endpoint, request.service]) payload = utils.xmlrpc_client.dumps(tuple(largs), From 0d7e32f627bb65fbd5c31e1ba55ce3e1019ab035 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 22 Sep 2014 18:07:29 -0500 Subject: [PATCH 0084/2592] Proxies missing a protocol raises differently in py2.6 + misc changes --- SoftLayer/tests/transport_tests.py | 18 ++++++++++++++---- SoftLayer/transports.py | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 4afec4f1c..5f166002b 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -4,6 +4,8 @@ :license: MIT, see LICENSE for more details. """ +import warnings + import mock import requests @@ -71,8 +73,12 @@ def test_proxy_without_protocol(self): req.method = 'Resource' req.proxy = 'localhost:3128' - self.assertRaises(SoftLayer.TransportError, - transports.make_xml_rpc_api_call, req) + try: + self.assertRaises(SoftLayer.TransportError, + transports.make_xml_rpc_api_call, req) + except AssertionError: + warnings.warn("AssertionError raised instead of a " + "SoftLayer.TransportError error") @mock.patch('requests.request') def test_valid_proxy(self, request): @@ -277,8 +283,12 @@ def test_proxy_without_protocol(self): req.method = 'Resource' req.proxy = 'localhost:3128' - self.assertRaises(SoftLayer.TransportError, - transports.make_rest_api_call, req) + try: + self.assertRaises(SoftLayer.TransportError, + transports.make_rest_api_call, req) + except AssertionError: + warnings.warn("AssertionError raised instead of a " + "SoftLayer.TransportError error") @mock.patch('requests.request') def test_valid_proxy(self, request): diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 2e9ff64c5..389e8b443 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -32,7 +32,7 @@ def __init__(self): self.method = None #: RPC Arguments. - self.args = [] + self.args = tuple() #: Transport headers. self.headers = {} @@ -43,7 +43,7 @@ def __init__(self): #: Integer timeout. self.timeout = None - #: URL to proxy to. + #: URL to proxy API requests to. self.proxy = None #: Verify HTTPS Certificate. From 0317a5606ec456dafc95869c586fa01bcbca4ac6 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 23 Sep 2014 10:28:20 -0500 Subject: [PATCH 0085/2592] Clarifies doc blocks --- SoftLayer/tests/transport_tests.py | 14 +++++++------- SoftLayer/transports.py | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 5f166002b..c655e3b53 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -58,7 +58,7 @@ def test_call(self, request): request.assert_called_with('POST', 'http://something.com/SoftLayer_Service', - headers=None, + headers={}, proxies=None, data=data, timeout=None, @@ -94,7 +94,7 @@ def test_valid_proxy(self, request): request.assert_called_with( 'POST', mock.ANY, - headers=None, + headers={}, proxies={'https': 'http://localhost:3128', 'http': 'http://localhost:3128'}, data=mock.ANY, @@ -248,7 +248,7 @@ def test_request_exception(self, request): class TestRestAPICall(testing.TestCase): @mock.patch('requests.request') - def test_json(self, request): + def test_basic(self, request): request().content = '{}' req = transports.Request() req.endpoint = 'http://something.com' @@ -259,7 +259,7 @@ def test_json(self, request): self.assertEqual(resp, {}) request.assert_called_with( 'GET', 'http://something.com/SoftLayer_Service/Resource.json', - headers=None, + headers={}, proxies=None, timeout=None) @@ -273,8 +273,8 @@ def test_json(self, request): }''' request().raise_for_status.side_effect = e - self.assertRaises( - SoftLayer.SoftLayerAPIError, transports.make_rest_api_call, req) + self.assertRaises(SoftLayer.SoftLayerAPIError, + transports.make_rest_api_call, req) def test_proxy_without_protocol(self): req = transports.Request() @@ -324,7 +324,7 @@ def test_with_id(self, request): request.assert_called_with( 'GET', 'http://something.com/SoftLayer_Service/getObject/2.json', - headers=None, + headers={}, proxies=None, timeout=None) diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 389e8b443..e199bea77 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -25,20 +25,20 @@ def __init__(self): #: The SoftLayer endpoint address. self.endpoint = None - #: Service name. + #: API service name. E.G. SoftLayer_Account self.service = None - #: Method name. + #: API method name. E.G. getObject self.method = None - #: RPC Arguments. + #: API Parameters. self.args = tuple() - #: Transport headers. + #: API headers, used for authentication, masks, limits, offsets, etc. self.headers = {} #: Transport headers. - self.transport_headers = None + self.transport_headers = {} #: Integer timeout. self.timeout = None @@ -46,10 +46,10 @@ def __init__(self): #: URL to proxy API requests to. self.proxy = None - #: Verify HTTPS Certificate. + #: Boolean specifying if the server certificate should be verified. self.verify = True - #: Client certificate path. + #: Client certificate file path. self.cert = None #: InitParameter/identifier of an object. @@ -111,7 +111,7 @@ def make_xml_rpc_api_call(request): timeout=request.timeout, verify=request.verify, cert=request.cert, - proxies=_proxies_dict(request.proxy)) + proxies=__proxies_dict(request.proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(response.headers) LOGGER.debug(response.content) @@ -163,7 +163,7 @@ def make_rest_api_call(request): resp = requests.request('GET', url, headers=request.transport_headers, timeout=request.timeout, - proxies=_proxies_dict(request.proxy)) + proxies=__proxies_dict(request.proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(resp.headers) LOGGER.debug(resp.content) @@ -177,8 +177,8 @@ def make_rest_api_call(request): raise exceptions.TransportError(0, str(ex)) -def _proxies_dict(proxy): - """Makes a dict appropriate to pass to requests.""" +def __proxies_dict(proxy): + """Makes a proxy dict appropriate to pass to requests.""" if not proxy: return None return {'http': proxy, 'https': proxy} From 99e5c21d202528e93748a2c789baad5b077f9ffa Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 23 Sep 2014 10:29:24 -0500 Subject: [PATCH 0086/2592] Fixes small style issue --- SoftLayer/API.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index cab5e6921..6797cf3b6 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -163,7 +163,7 @@ def call(self, service, method, *args, **kwargs): request.method = method request.args = args request.transport_headers = http_headers - request.timeout = self. timeout + request.timeout = self.timeout request.proxy = self.proxy request.identifier = kwargs.get('id') request.mask = kwargs.get('mask') From a399890fe6791d64d856e4592adb5e5ca07dd4ba Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 1 Oct 2014 13:59:04 -0500 Subject: [PATCH 0087/2592] Adds version to main CLI help --- SoftLayer/CLI/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index fa383eb74..c47b24a54 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -4,7 +4,7 @@ sl help sl [-h | --help] -SoftLayer Command-line Client +SoftLayer Command-line Client {version} The available modules are: @@ -98,7 +98,8 @@ def __init__(self, env): def get_main_help(self): """Get main help text.""" - return _append_common_options(__doc__).strip() + main_doc = __doc__.format(version=SoftLayer.__version__) + return _append_common_options(main_doc).strip() def get_module_help(self, module_name): """Get help text for a module.""" From a6873a0d11e9058ec839aa562d10fc2b90876e51 Mon Sep 17 00:00:00 2001 From: hack Date: Thu, 9 Oct 2014 15:01:11 +0200 Subject: [PATCH 0088/2592] Support dash (-) in hostname when importing dns zone files (issue #146) --- SoftLayer/CLI/modules/dns.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 96e43ee9c..9b1cb7ee0 100644 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -100,11 +100,11 @@ def execute(self,args): manager = SoftLayer.DNSManager(self.client) lines = [line.strip() for line in open(args[''])] - zoneSearch = re.search('\$ORIGIN (?P.*)\.',lines[0]) + zoneSearch = re.search('^\$ORIGIN (?P.*)\.',lines[0]) zone = zoneSearch.group('zone') if (dryRun): - print "Starting up a dry run..." + print "Starting up a dry run for %s..." % (zone) zone_id = 0 else: try: @@ -114,9 +114,11 @@ def execute(self,args): manager.create_zone(zone) zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') - for content in lines: - domainSearch = re.search('((?P(\w+(\.)?)*|\@)?\s+(?P\d+)?\s+(?P\w+)?)?\s+(?P\w+)\s+(?P.*)',content) - if (domainSearch is not None): + for content in lines[1:]: + domainSearch = re.search('^((?P([\w-]+(\.)?)*|\@)?\s+(?P\d+)?\s+(?P\w+)?)?\s+(?P\w+)\s+(?P.*)',content) + if (domainSearch is None): + print "\033[92mFailed: unknown line: %s\033[0m" % (content) + else: domainName = domainSearch.group('domain') #The API requires we send a host, although bind allows a blank entry. @ is the same thing as blank if (domainName is None): From 47c72d598df80d79773bff068f7e44699da26ab1 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 10 Oct 2014 14:47:38 +0700 Subject: [PATCH 0089/2592] Revert "Issue #146 - DNS Zone Import" --- SoftLayer/CLI/modules/dns.py | 73 ------------------- .../tests/CLI/modules/artifacts/realtest.com | 21 ------ SoftLayer/tests/CLI/modules/dns_tests.py | 12 --- 3 files changed, 106 deletions(-) mode change 100644 => 100755 SoftLayer/CLI/modules/dns.py delete mode 100644 SoftLayer/tests/CLI/modules/artifacts/realtest.com diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py old mode 100644 new mode 100755 index 9b1cb7ee0..0d9d6f389 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -8,7 +8,6 @@ delete Delete zone list List zones or a zone's records print Print zone in BIND format - import Import a BIND style zone file The available record commands are: add Add resource record @@ -81,78 +80,6 @@ def execute(self, args): raise exceptions.CLIAbort("Aborted.") -class ImportZone(environment.CLIRunnable): - """ -usage: sl dns import [--dryRun] - -Creates a new zone based off a nicely BIND formatted file - -Arguments: - Path to the bind zone file you want to import -Options: - --dryRun don't actually do anything. This will show you what we were able to parse. - - """ - action = 'import' - def execute(self,args): - import re - dryRun = args.get('--dryRun') - - manager = SoftLayer.DNSManager(self.client) - lines = [line.strip() for line in open(args[''])] - zoneSearch = re.search('^\$ORIGIN (?P.*)\.',lines[0]) - zone = zoneSearch.group('zone') - - if (dryRun): - print "Starting up a dry run for %s..." % (zone) - zone_id = 0 - else: - try: - zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') - except : - print "\033[92mCREATED ZONE: %s\033[0m" % (zone) - manager.create_zone(zone) - zone_id = helpers.resolve_id(manager.resolve_ids, zone,name='zone') - - for content in lines[1:]: - domainSearch = re.search('^((?P([\w-]+(\.)?)*|\@)?\s+(?P\d+)?\s+(?P\w+)?)?\s+(?P\w+)\s+(?P.*)',content) - if (domainSearch is None): - print "\033[92mFailed: unknown line: %s\033[0m" % (content) - else: - domainName = domainSearch.group('domain') - #The API requires we send a host, although bind allows a blank entry. @ is the same thing as blank - if (domainName is None): - domainName = "@" - - domainttl = domainSearch.group('ttl') - domainClass = domainSearch.group('class') - domainType = domainSearch.group('type') - domainRecord = domainSearch.group('record') - - #This will skip the SOA record bit. And any domain that gets parsed oddly. - if (domainType.upper() == 'IN'): - print "SKIPPED: Host: %s TTL: %s Type: %s Record: %s" % (domainName,domainttl,domainType,domainRecord) - continue - - #the dns class doesn't support weighted MX records yet, so we chomp that part out. - if (domainType.upper() == "MX"): - recordSearch = re.search('(?P\d+)\s+(?P.*)',domainRecord) - domainRecord = recordSearch.group('record') - - try: - if (dryRun): - print "Parsed: Host: %s TTL: %s Type: %s Record: %s" % (domainName,domainttl,domainType,domainRecord) - else: - manager.create_record(zone_id,domainName,domainType,domainRecord,domainttl) - print "\033[92mCreated: Host: %s TTL: %s Type: %s Record: %s\033[0m" % (domainName,domainttl,domainType,domainRecord) - except Exception, e: - print "\033[91mFAILED: Host: %s Type: %s Record: %s" % (domainName,domainType,domainRecord.upper()) - print "\t", e ,"\033[0m" - - - return "Finished" - - class ListZones(environment.CLIRunnable): """ usage: sl dns list [] [options] diff --git a/SoftLayer/tests/CLI/modules/artifacts/realtest.com b/SoftLayer/tests/CLI/modules/artifacts/realtest.com deleted file mode 100644 index 0c5550d1d..000000000 --- a/SoftLayer/tests/CLI/modules/artifacts/realtest.com +++ /dev/null @@ -1,21 +0,0 @@ -$ORIGIN realtest.com. -$TTL 86400 -@ IN SOA ns1.softlayer.com. support.softlayer.com. ( - 2014052300 ; Serial - 7200 ; Refresh - 600 ; Retry - 1728000 ; Expire - 43200) ; Minimum - -@ 86400 IN NS ns1.softlayer.com. -@ 86400 IN NS ns2.softlayer.com. - - IN MX 10 test.realtest.com. -testing 86400 IN A 127.0.0.1 -testing1 86400 IN A 12.12.0.1 -server2 IN A 1.0.3.4 -ftp IN CNAME server2 -dev.realtest.com IN TXT "This is just a test of the txt record" - IN AAAA 2001:db8:10::1 -spf IN TXT "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all" - diff --git a/SoftLayer/tests/CLI/modules/dns_tests.py b/SoftLayer/tests/CLI/modules/dns_tests.py index b4986a0d5..811556db2 100644 --- a/SoftLayer/tests/CLI/modules/dns_tests.py +++ b/SoftLayer/tests/CLI/modules/dns_tests.py @@ -119,15 +119,3 @@ def test_delete_record(self, no_going_back_mock): '': 'hostname', '--id': 1, '--really': False}) - - def test_import_zone(self): - import pprint - pp = pprint.PrettyPrinter(indent=4) - - command = dns.ImportZone(client=self.client) - output = command.execute({ - '' : 'realtest.com' - '--dryRun' : '--dryRun' - }) - pp.pprint(output) - self.assertEqual(['Finished'],output) \ No newline at end of file From a31bfd67730fa5d5a2f5f011ee57cce362b20aad Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 08:13:54 -0500 Subject: [PATCH 0090/2592] Many fixes to DNS import code Fixes tests (the existing test was failing and didn't actually test) Fixes styling (flake8 and pylint were crying) Moves parsing logic away from the logic to persist changes to SoftLayer --- SoftLayer/CLI/modules/dns.py | 114 +++++++++++++++++++ SoftLayer/managers/dns.py | 1 + SoftLayer/testing/fixtures/realtest.com | 21 ++++ SoftLayer/tests/CLI/modules/dns_tests.py | 134 +++++++++++++++++++++++ 4 files changed, 270 insertions(+) create mode 100644 SoftLayer/testing/fixtures/realtest.com diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 0d9d6f389..90a995b87 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -22,6 +22,14 @@ from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +import re + +RECORD_REGEX = re.compile(r"""^((?P([\w-]+(\.)?)*|\@)?\s+ + (?P\d+)?\s+ + (?P\w+)?)?\s+ + (?P\w+)\s+ + (?P.*)""", re.X) + class DumpZone(environment.CLIRunnable): """ @@ -80,6 +88,112 @@ def execute(self, args): raise exceptions.CLIAbort("Aborted.") +class ImportZone(environment.CLIRunnable): + """ +usage: sl dns import [options] + +Creates a new zone based off a nicely BIND formatted file + +Arguments: + Path to the bind zone file you want to import +Options: + --dry-run Don't actually do anything. This will show you what is parsed + + """ + action = 'import' + + def execute(self, args): + + dry_run = args.get('--dry-run') + + manager = SoftLayer.DNSManager(self.client) + with open(args['']) as zone_file: + zone_contents = zone_file.read() + + zone, records, bad_lines = parse_zone_details(zone_contents) + + self.env.out("Parsed: zone=%s" % zone) + for record in records: + self.env.out("Parsed: %s" % record) + for line in bad_lines: + self.env.out("Unparsed: %s" % line) + + if dry_run: + return + + # Find zone id or create the zone if it doesn't exist + try: + zone_id = helpers.resolve_id(manager.resolve_ids, zone, + name='zone') + except exceptions.CLIAbort: + zone_id = manager.create_zone(zone)['id'] + self.env.out("\033[92mCREATED ZONE: %s\033[0m" % zone) + + # Attempt to create each record + for record in records: + try: + manager.create_record(zone_id, + record['record'], + record['record_type'], + record['data'], + record['ttl']) + self.env.out("\033[92mCreated: Host: %s\033[0m" % record) + except SoftLayer.SoftLayerAPIError as ex: + self.env.out("\033[91mFAILED: %s" % record) + self.env.out("%s \033[0m" % ex) + + return "Finished" + + +def parse_zone_details(zone_contents): + """Parses a zone file into python data-structures""" + records = [] + bad_lines = [] + zone_lines = [line.strip() for line in zone_contents.split('\n')] + + zone_search = re.search(r'^\$ORIGIN (?P.*)\.', zone_lines[0]) + zone = zone_search.group('zone') + + for line in zone_lines[1:]: + record_search = re.search(RECORD_REGEX, line) + if record_search is None: + bad_lines.append(line) + continue + + name = record_search.group('domain') + # The API requires we send a host, although bind allows a blank + # entry. @ is the same thing as blank + if name is None: + name = "@" + + ttl = record_search.group('ttl') + # we don't do anything with the class + # domain_class = domainSearch.group('class') + record_type = record_search.group('type').upper() + data = record_search.group('data') + + # the dns class doesn't support weighted MX records yet, so we chomp + # that part out. + if record_type == "MX": + record_search = re.search(r'(?P\d+)\s+(?P.*)', data) + data = record_search.group('data') + + # This will skip the SOA record bit. And any domain that gets + # parsed oddly. + if record_type == 'IN': + bad_lines.append(line) + continue + + records.append({ + 'record': name, + 'record_type': record_type, + 'data': data, + 'ttl': ttl, + }) + + return zone, records, bad_lines + + class ListZones(environment.CLIRunnable): """ usage: sl dns list [] [options] diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index 6aa9d5429..c0dc5d221 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -21,6 +21,7 @@ def __init__(self, client): self.client = client self.service = self.client['Dns_Domain'] self.record = self.client['Dns_Domain_ResourceRecord'] + self.resolvers = [self._get_zone_id_from_name] def _get_zone_id_from_name(self, name): diff --git a/SoftLayer/testing/fixtures/realtest.com b/SoftLayer/testing/fixtures/realtest.com new file mode 100644 index 000000000..0c5550d1d --- /dev/null +++ b/SoftLayer/testing/fixtures/realtest.com @@ -0,0 +1,21 @@ +$ORIGIN realtest.com. +$TTL 86400 +@ IN SOA ns1.softlayer.com. support.softlayer.com. ( + 2014052300 ; Serial + 7200 ; Refresh + 600 ; Retry + 1728000 ; Expire + 43200) ; Minimum + +@ 86400 IN NS ns1.softlayer.com. +@ 86400 IN NS ns2.softlayer.com. + + IN MX 10 test.realtest.com. +testing 86400 IN A 127.0.0.1 +testing1 86400 IN A 12.12.0.1 +server2 IN A 1.0.3.4 +ftp IN CNAME server2 +dev.realtest.com IN TXT "This is just a test of the txt record" + IN AAAA 2001:db8:10::1 +spf IN TXT "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all" + diff --git a/SoftLayer/tests/CLI/modules/dns_tests.py b/SoftLayer/tests/CLI/modules/dns_tests.py index 811556db2..e9fb70783 100644 --- a/SoftLayer/tests/CLI/modules/dns_tests.py +++ b/SoftLayer/tests/CLI/modules/dns_tests.py @@ -4,6 +4,8 @@ :license: MIT, see LICENSE for more details. """ +import os.path + import mock from SoftLayer.CLI import exceptions @@ -16,6 +18,7 @@ class DnsTests(testing.TestCase): def set_up(self): self.client = testing.FixtureClient() + self.env = mock.MagicMock() def test_dump_zone(self): command = dns.DumpZone(client=self.client) @@ -119,3 +122,134 @@ def test_delete_record(self, no_going_back_mock): '': 'hostname', '--id': 1, '--really': False}) + + def test_parse_zone_file(self): + zone_file = """$ORIGIN realtest.com. +$TTL 86400 +@ IN SOA ns1.softlayer.com. support.softlayer.com. ( + 2014052300 ; Serial + 7200 ; Refresh + 600 ; Retry + 1728000 ; Expire + 43200) ; Minimum + +@ 86400 IN NS ns1.softlayer.com. +@ 86400 IN NS ns2.softlayer.com. + + IN MX 10 test.realtest.com. +testing 86400 IN A 127.0.0.1 +testing1 86400 IN A 12.12.0.1 +server2 IN A 1.0.3.4 +ftp IN CNAME server2 +dev.realtest.com IN TXT "This is just a test of the txt record" + IN AAAA 2001:db8:10::1 +spf IN TXT "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a" + +""" + expected = [{'data': 'ns1.softlayer.com.', + 'record': '@', + 'record_type': 'NS', + 'ttl': '86400'}, + {'data': 'ns2.softlayer.com.', + 'record': '@', + 'record_type': 'NS', + 'ttl': '86400'}, + {'data': '127.0.0.1', + 'record': 'testing', + 'record_type': 'A', + 'ttl': '86400'}, + {'data': '12.12.0.1', + 'record': 'testing1', + 'record_type': 'A', + 'ttl': '86400'}, + {'data': '1.0.3.4', + 'record': 'server2', + 'record_type': 'A', + 'ttl': None}, + {'data': 'server2', + 'record': 'ftp', + 'record_type': 'CNAME', + 'ttl': None}, + {'data': '"This is just a test of the txt record"', + 'record': 'dev.realtest.com', + 'record_type': 'TXT', + 'ttl': None}, + {'data': '"v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a"', + 'record': 'spf', + 'record_type': 'TXT', + 'ttl': None}] + zone, records, bad_lines = dns.parse_zone_details(zone_file) + self.assertEqual(zone, 'realtest.com') + self.assertEqual(records, expected) + self.assertEqual(len(bad_lines), 13) + + def test_import_zone_dry_run(self): + command = dns.ImportZone(client=self.client, env=self.env) + path = os.path.join(testing.FIXTURE_PATH, 'realtest.com') + output = command.execute({ + '': path, + '--dry-run': True, + }) + + # Dry run should not result in create calls + self.assertFalse(self.client['Dns_Domain'].createObject.called) + record_service = self.client['Dns_Domain_ResourceRecord'] + self.assertFalse(record_service.createObject.called) + + self.assertEqual(None, output) + + def test_import_zone(self): + command = dns.ImportZone(client=self.client, env=self.env) + path = os.path.join(testing.FIXTURE_PATH, 'realtest.com') + output = command.execute({ + '': path, + '--dry-run': False, + }) + + self.assertFalse(self.client['Dns_Domain'].createObject.called) + record_service = self.client['Dns_Domain_ResourceRecord'] + self.assertEqual(record_service.createObject.call_args_list, + [mock.call({'data': 'ns1.softlayer.com.', + 'host': '@', + 'domainId': 12345, + 'type': 'NS', + 'ttl': '86400'}), + mock.call({'data': 'ns2.softlayer.com.', + 'host': '@', + 'domainId': 12345, + 'type': 'NS', + 'ttl': '86400'}), + mock.call({'data': '127.0.0.1', + 'host': 'testing', + 'domainId': 12345, + 'type': 'A', + 'ttl': '86400'}), + mock.call({'data': '12.12.0.1', + 'host': 'testing1', + 'domainId': 12345, + 'type': 'A', + 'ttl': '86400'}), + mock.call({'data': '1.0.3.4', + 'host': 'server2', + 'domainId': 12345, + 'type': 'A', + 'ttl': None}), + mock.call({'data': 'server2', + 'host': 'ftp', + 'domainId': 12345, + 'type': 'CNAME', + 'ttl': None}), + mock.call({'data': + '"This is just a test of the txt record"', + 'host': 'dev.realtest.com', + 'domainId': 12345, + 'type': 'TXT', + 'ttl': None}), + mock.call({'data': '"v=spf1 ip4:192.0.2.0/24 ' + 'ip4:198.51.100.123 a -all"', + 'host': 'spf', + 'domainId': 12345, + 'type': 'TXT', + 'ttl': None})]) + + self.assertEqual("Finished", output) From 9c08eb06e141ae72704f169d346d1e5a8cd96ec2 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 08:41:26 -0500 Subject: [PATCH 0091/2592] Undo unintended new line --- SoftLayer/managers/dns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index c0dc5d221..6aa9d5429 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -21,7 +21,6 @@ def __init__(self, client): self.client = client self.service = self.client['Dns_Domain'] self.record = self.client['Dns_Domain_ResourceRecord'] - self.resolvers = [self._get_zone_id_from_name] def _get_zone_id_from_name(self, name): From a8fa357e4b034df354a514151938dfcf6e4ca437 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 12:07:48 -0500 Subject: [PATCH 0092/2592] Improves ordering by quote interface --- SoftLayer/managers/ordering.py | 65 +++++++++++++--------- SoftLayer/tests/managers/ordering_tests.py | 44 +++++++++++---- 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index d99d9dcbb..e344d79e9 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -27,7 +27,8 @@ def get_packages_of_type(self, package_types, mask): type keynames we are interested in. :param string mask: Mask to specify the properties we want to retrieve """ - package_service = self.get_package_service() + + package_service = self.client['Product_Package'] _filter = { 'type': { 'keyName': { @@ -44,12 +45,6 @@ def get_packages_of_type(self, package_types, mask): packages = self.filter_outlet_packages(packages) return packages - def get_package_service(self): - """ Get the service to query product packages - :return SoftLayer.API.Service - """ - return self.client['Product_Package'] - @staticmethod def filter_outlet_packages(packages): """ Remove packages designated as OUTLET @@ -60,6 +55,7 @@ def filter_outlet_packages(packages): :param packages: Dictionary of packages. Name and description keys must be present in each of them. """ + non_outlet_packages = [] for package in packages: @@ -78,6 +74,7 @@ def get_only_active_packages(packages): :param packages Dictionary of packages, isActive key must be present """ + active_packages = [] for package in packages: @@ -110,6 +107,7 @@ def get_package_id_by_type(self, package_type): we are interested in :raises ValueError when no package of the given type is found """ + mask = "mask[id, name, description, isActive, type[keyName]]" package = self.get_package_by_type(package_type, mask) if package: @@ -119,55 +117,65 @@ def get_package_id_by_type(self, package_type): def get_quotes(self): """ Retrieve a list of quotes + :return a list of SoftLayer_Billing_Order_Quote """ + quotes = self.client['Account'].getActiveQuotes() return quotes def get_quote_details(self, quote_id): """ Retrieve quote details + :param quote_id ID number of target quote """ + quote = self.client['Billing_Order_Quote'].getObject(id=quote_id) return quote def get_order_container(self, quote_id): """ Generate an order container from a quote object + :param quote_id ID number of target quote """ + quote = self.client['Billing_Order_Quote'] container = quote.getRecalculatedOrderContainer(id=quote_id) return container['orderContainers'][0] - def generate_order_template(self, quote_id=None, hostnames=None, - domain=None, quantity=None): + def generate_order_template(self, quote_id, extra, quantity=1): """ Generate a complete order template + :param int quote_id: ID of target quote - :param list hostnames: List of hostnames as strings - :param string domain: Domain name to be used for all servers - :param int quantity: Quantity to override default + :param list extra: List of dictionaries that have extra details about + the order such as hostname or domain names for + virtual servers or hardware nodes + :param int quantity: Number of ~things~ to order """ + container = self.get_order_container(quote_id) - if quantity is not None: - container['quantity'] = quantity + container['quantity'] = quantity + + # TODO(kmcdonald): This will only work with virtualGuests and hardware. + # There has to be a better way, since this is based on + # an existing quote that supposedly knows about this + # detail if container['packageId'] == 46: product_type = 'virtualGuests' else: product_type = 'hardware' - if len(hostnames) != container['quantity']: - raise ValueError("You must specify a hostname for each " - "server in the quote") + if len(extra) != quantity: + raise ValueError("You must specify extra for each server in the " + "quote") container[product_type] = [] - for hostname in hostnames: - container[product_type].append( - {'hostname': hostname, 'domain': domain} - ) + for extra_details in extra: + container[product_type].append(extra_details) container['presetId'] = None return container - def verify_quote(self, **kwargs): + def verify_quote(self, quote_id, extra, quantity=1): """ Verifies that a quote order is valid without actually ordering the resources @@ -177,17 +185,20 @@ def verify_quote(self, **kwargs): :param string domain: domain of the new servers :param int quantity: Quantity to override default """ - container = self.generate_order_template(**kwargs) + + container = self.generate_order_template(quote_id, extra, + quantity=quantity) return self.client['Product_Order'].verifyOrder(container) - def order_quote(self, **kwargs): - """ - Places an order using a quote + def order_quote(self, quote_id, extra, quantity=1): + """ Places an order using a quote :param int quote_id: ID for the target quote :param list hostnames: hostnames of the servers :param string domain: domain of the new server :param int quantity: Quantity to override default """ - container = self.generate_order_template(**kwargs) + + container = self.generate_order_template(quote_id, extra, + quantity=quantity) return self.client['Product_Order'].placeOrder(container) diff --git a/SoftLayer/tests/managers/ordering_tests.py b/SoftLayer/tests/managers/ordering_tests.py index 66ffcda86..025e31af3 100644 --- a/SoftLayer/tests/managers/ordering_tests.py +++ b/SoftLayer/tests/managers/ordering_tests.py @@ -1,6 +1,6 @@ """ SoftLayer.tests.managers.ordering_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :license: MIT, see LICENSE for more details. """ @@ -83,19 +83,41 @@ def test_get_quotes(self): def test_get_quote_details(self): quote = self.ordering.get_quote_details(1234) - quote_fixture = self.ordering.client['Billing_Order_Quote'].getObject( - id=1234) + quote_service = self.ordering.client['Billing_Order_Quote'] + quote_fixture = quote_service.getObject(id=1234) self.assertEqual(quote, quote_fixture) def test_verify_quote(self): - result = self.ordering.verify_quote( - quote_id=1234, - domain='example.com', - hostnames=['test1'], - quantity=1) + order_service = self.ordering.client['Product_Order'] + result = self.ordering.verify_quote(1234, + [{'hostname': 'test1', + 'domain': 'example.com'}], + quantity=1) - self.assertEqual(result, self.ordering.client['Product_Order']. - verifyOrder()) + self.assertEqual(result, order_service.verifyOrder()) + self.assertTrue(order_service.verifyOrder.called) def test_order_quote(self): - return True + order_service = self.ordering.client['Product_Order'] + result = self.ordering.verify_quote(1234, + [{'hostname': 'test1', + 'domain': 'example.com'}], + quantity=1) + + self.assertEqual(result, order_service.placeOrder()) + self.assertTrue(order_service.placeOrder.called) + + def test_generate_order_template(self): + result = self.ordering.generate_order_template( + 1234, [{'hostname': 'test1', 'domain': 'example.com'}], quantity=1) + self.assertEqual(result, {'presetId': None, + 'hardware': [{'domain': 'example.com', + 'hostname': 'test1'}], + 'useHourlyPricing': '', + 'packageId': 50, + 'prices': [{'id': 1921}], + 'quantity': 1}) + + def test_generate_order_template_extra_quantity(self): + with self.assertRaises(ValueError): + self.ordering.generate_order_template(1234, [], quantity=1) From 4eec144a13fefa37de131c77613bd46619b45645 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 12:20:12 -0500 Subject: [PATCH 0093/2592] Changes TODO to NOTE --- SoftLayer/managers/ordering.py | 2 +- SoftLayer/managers/vs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index e344d79e9..874213e7f 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -156,7 +156,7 @@ def generate_order_template(self, quote_id, extra, quantity=1): container = self.get_order_container(quote_id) container['quantity'] = quantity - # TODO(kmcdonald): This will only work with virtualGuests and hardware. + # NOTE(kmcdonald): This will only work with virtualGuests and hardware. # There has to be a better way, since this is based on # an existing quote that supposedly knows about this # detail diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index d8538fc5b..f558d7501 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -642,7 +642,7 @@ def _get_package_items(self): mask = "mask[description,capacity,prices[id,categories[name,id]]]" package_type = "VIRTUAL_SERVER_INSTANCE" package_id = self.ordering_manager.get_package_id_by_type(package_type) - package_service = self.ordering_manager.get_package_service() + package_service = self.client['Product_Package'] return package_service.getItems(id=package_id, mask=mask) From 7577307c1b2fca9d4d403b9de23ab6e34b674694 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 12:47:19 -0500 Subject: [PATCH 0094/2592] Version bump to 3.3.0 --- CHANGELOG | 18 +++++++++++++++++- SoftLayer/consts.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4e352b227..341db6be2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,19 @@ +3.3.0 + + * CLI+API: Load balancer support + + * CLI: More detail added to the `sl image detail` and `sl image list` commands + + * CLI+API: Adds support for booting into rescue images for virtual servers and hardware + + * API: Adds ability to order virtual and hardare servers from a quote to the ordering manager + + * CLI: Fixes bug with sl server list-chassis and sl server list-chassis + + * API: Restructure of the way custom authentication can be plugged in the API client + + * Several other bug fixes + 3.2.0 * CLI+API: Added firewall manager and CLI module @@ -8,7 +24,7 @@ * API: Added OrderingManager. Remove hard-coded price IDs - * Fixed several small bug fixes + * Fixed several small bugs 3.1.0 diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 6ee3ddc47..dc1c8c386 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v3.2.0' +VERSION = 'v3.3.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 31889fed7..f7dffe596 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '3.2.0' +version = '3.3.0' # The full version, including alpha/beta/rc tags. -release = '3.2.0' +release = '3.3.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 c0c434b0e..a3e4df4ba 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ setup( name='SoftLayer', - version='3.2.0', + version='3.3.0', description=description, long_description=long_description, author='SoftLayer Technologies, Inc.', From 36e976f848372115cfa057056856303879890c96 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 17:25:44 -0500 Subject: [PATCH 0095/2592] Cleanup firewall and load balancer code --- SoftLayer/managers/firewall.py | 43 +++++------ SoftLayer/managers/load_balancer.py | 87 +++++++++++----------- SoftLayer/tests/managers/firewall_tests.py | 10 +-- SoftLayer/tests/managers/loadbal_tests.py | 14 ++-- 4 files changed, 74 insertions(+), 80 deletions(-) diff --git a/SoftLayer/managers/firewall.py b/SoftLayer/managers/firewall.py index ef9ad74f3..c45fdaf3d 100644 --- a/SoftLayer/managers/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -29,7 +29,6 @@ def has_firewall(vlan): class FirewallManager(utils.IdentifierMixin, object): - """ Manages firewalls. :param SoftLayer.API.Client client: the API client instance @@ -49,17 +48,13 @@ def get_standard_package(self, server_id, is_cci=True): False for a server :returns: A dictionary containing the standard CCI firewall package """ + firewall_port_speed = self._get_fwl_port_speed(server_id, is_cci) - _filter = utils.NestedDict({}) - _value = "%s%s" % (firewall_port_speed, - "Mbps Hardware Firewall") - _filter['items']['description'] = utils.query_filter(_value) + _value = "%s%s" % (firewall_port_speed, "Mbps Hardware Firewall") + _filter = {'items': {'description': utils.query_filter(_value)}} - kwargs = utils.NestedDict({}) - kwargs['id'] = 0 # look at package id 0 - kwargs['filter'] = _filter.to_dict() - return self.prod_pkg.getItems(**kwargs) + return self.prod_pkg.getItems(id=0, filter=_filter) def get_dedicated_package(self, ha_enabled=False): """ Retrieves the dedicated firewall package. @@ -77,10 +72,7 @@ def get_dedicated_package(self, ha_enabled=False): else: _filter['items']['description'] = utils.query_filter(fwl_filter) - kwargs = utils.NestedDict({}) - kwargs['id'] = 0 # look at package id 0 - kwargs['filter'] = _filter.to_dict() - return self.prod_pkg.getItems(**kwargs) + return self.prod_pkg.getItems(id=0, filter=_filter.to_dict()) def cancel_firewall(self, firewall_id, dedicated=False): """ Cancels the specified firewall. @@ -89,6 +81,7 @@ def cancel_firewall(self, firewall_id, dedicated=False): :param bool dedicated: If true, the firewall instance is dedicated, otherwise, the firewall instance is shared. """ + fwl_billing = self._get_fwl_billing_item(firewall_id, dedicated) billing_id = fwl_billing['billingItem']['id'] billing_item = self.client['Billing_Item'] @@ -102,6 +95,7 @@ def add_standard_firewall(self, server_id, is_cci=True): otherwise for a CCI :returns: A dictionary containing the standard CCI firewall order """ + package = self.get_standard_package(server_id, is_cci) if is_cci: product_order = { @@ -131,6 +125,7 @@ def add_vlan_firewall(self, vlan_id, ha_enabled=False): :returns: A dictionary containing the VLAN firewall order """ + package = self.get_dedicated_package(ha_enabled) product_order = { 'complexType': 'SoftLayer_Container_Product_Order_Network_' @@ -149,6 +144,7 @@ def _get_fwl_billing_item(self, firewall_id, dedicated=False): :param bool dedicated: whether the firewall is dedicated or standard :returns: A dictionary of the firewall billing item. """ + mask = ('mask[id,billingItem[id]]') if dedicated: fwl_svc = self.client['Network_Vlan_Firewall'] @@ -163,15 +159,15 @@ def _get_fwl_port_speed(self, server_id, is_cci=True): :param bool is_cci: true if the server_id is for a virtual server :returns: a integer representing the Mbps speed of a firewall """ + fwl_port_speed = 0 if is_cci: - mask = ('mask[primaryNetworkComponent[maxSpeed]]') + mask = ('primaryNetworkComponent[maxSpeed]') svc = self.client['Virtual_Guest'] primary = svc.getObject(mask=mask, id=server_id) fwl_port_speed = primary['primaryNetworkComponent']['maxSpeed'] else: - mask = ('mask[id,maxSpeed,' - 'networkComponentGroup.networkComponents]') + mask = ('id,maxSpeed,networkComponentGroup.networkComponents') svc = self.client['Hardware_Server'] network_components = svc.getFrontendNetworkComponents( mask=mask, id=server_id) @@ -207,6 +203,7 @@ def get_firewalls(self): :returns: A list of firewalls on the current account. """ + mask = ('firewallNetworkComponents,' 'networkVlanFirewall,' 'dedicatedFirewallFlag,' @@ -225,6 +222,7 @@ def get_standard_fwl_rules(self, firewall_id): :param integer firewall_id: the instance ID of the standard firewall :returns: A list of the rules. """ + svc = self.client['Network_Component_Firewall'] return svc.getRules(id=firewall_id, mask=RULE_MASK) @@ -234,6 +232,7 @@ def get_dedicated_fwl_rules(self, firewall_id): :param integer firewall_id: the instance ID of the dedicated firewall :returns: A list of the rules. """ + svc = self.client['Network_Vlan_Firewall'] return svc.getRules(id=firewall_id, mask=RULE_MASK) @@ -243,6 +242,7 @@ def edit_dedicated_fwl_rules(self, firewall_id, rules): :param integer firewall_id: the instance ID of the dedicated firewall :param dict rules: the rules to be pushed on the firewall """ + mask = ('mask[networkVlan[firewallInterfaces' '[firewallContextAccessControlLists]]]') svc = self.client['Network_Vlan_Firewall'] @@ -257,10 +257,8 @@ def edit_dedicated_fwl_rules(self, firewall_id, rules): continue fwl_ctx_acl_id = control_list['id'] - template = { - 'firewallContextAccessControlListId': fwl_ctx_acl_id, - 'rules': rules - } + template = {'firewallContextAccessControlListId': fwl_ctx_acl_id, + 'rules': rules} svc = self.client['Network_Firewall_Update_Request'] return svc.createObject(template) @@ -271,9 +269,8 @@ def edit_standard_fwl_rules(self, firewall_id, rules): :param integer firewall_id: the instance ID of the standard firewall :param dict rules: the rules to be pushed on the firewall """ + rule_svc = self.client['Network_Firewall_Update_Request'] - template = { - "networkComponentFirewallId": firewall_id, - "rules": rules} + template = {'networkComponentFirewallId': firewall_id, 'rules': rules} return rule_svc.createObject(template) diff --git a/SoftLayer/managers/load_balancer.py b/SoftLayer/managers/load_balancer.py index 883df6bb3..7d441572f 100644 --- a/SoftLayer/managers/load_balancer.py +++ b/SoftLayer/managers/load_balancer.py @@ -9,8 +9,8 @@ class LoadBalancerManager(utils.IdentifierMixin, object): - """ Manages load balancers. + :param SoftLayer.API.Client client: the API client instance """ @@ -27,14 +27,10 @@ def get_lb_pkgs(self): :returns: A dictionary containing the load balancer packages """ - lb_filter = '*Load Balancer*' - _filter = utils.NestedDict({}) - _filter['items']['description'] = utils.query_filter(lb_filter) + _filter = {'items': {'description': + utils.query_filter('*Load Balancer*')}} - kwargs = utils.NestedDict({}) - kwargs['id'] = 0 # look at package id 0 - kwargs['filter'] = _filter.to_dict() - packages = self.prod_pkg.getItems(**kwargs) + packages = self.prod_pkg.getItems(id=0, filter=_filter) pkgs = [] for package in packages: if not package['description'].startswith('Global'): @@ -46,6 +42,7 @@ def get_ip_address(self, ip_address=None): :returns: A dictionary containing the IP address properties """ + svc = self.client['Network_Subnet_IpAddress'] return svc.getByIpAddress(ip_address) @@ -54,6 +51,7 @@ def get_hc_types(self): :returns: A dictionary containing the health check types """ + svc = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_Health_Check_Type'] return svc.getAllObjects() @@ -63,6 +61,7 @@ def get_routing_methods(self): :returns: A dictionary containing the load balancer routing methods """ + svc = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_Routing_Method'] return svc.getAllObjects() @@ -72,17 +71,19 @@ def get_routing_types(self): :returns: A dictionary containing the load balancer routing types """ + svc = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_Routing_Type'] return svc.getAllObjects() - def get_location(self, datacenter): + def _get_location(self, datacenter): """ Returns the location of the specified datacenter :param string datacenter: The datacenter to create the loadbalancer in :returns: the location id of the given datacenter """ + dcenters = self.client['Location'].getDataCenters() for dcenter in dcenters: if dcenter['name'] == datacenter: @@ -94,6 +95,7 @@ def cancel_lb(self, loadbal_id): :param int loadbal_id: Load Balancer ID to be cancelled. """ + lb_billing = self.lb_svc.getBillingItem(id=loadbal_id) billing_id = lb_billing['id'] billing_item = self.client['Billing_Item'] @@ -104,15 +106,15 @@ def add_local_lb(self, price_item_id, datacenter): :param int price_item_id: The price item ID for the load balancer :param string datacenter: The datacenter to create the loadbalancer in - :returns: A dictionary containing the product order """ + product_order = { 'complexType': 'SoftLayer_Container_Product_Order_Network_' 'LoadBalancer', 'quantity': 1, 'packageId': 0, - "location": self.get_location(datacenter), + "location": self._get_location(datacenter), 'prices': [{'id': price_item_id}] } return self.client['Product_Order'].placeOrder(product_order) @@ -122,16 +124,17 @@ def get_local_lbs(self): :returns: A list of all local load balancers on the current account. """ + mask = ('mask[loadBalancerHardware[datacenter],ipAddress]') return self.account.getAdcLoadBalancers(mask=mask) def get_local_lb(self, loadbal_id, **kwargs): """ Returns a specified local load balancer given the id. - :param int loadbal_id: The id of the load balancer to retrieve + :param int loadbal_id: The id of the load balancer to retrieve :returns: A dictionary containing the details of the load balancer """ - # virtualServers.serviceGroups.services.ipAddress + if 'mask' not in kwargs: kwargs['mask'] = ('mask[loadBalancerHardware[datacenter], ' 'ipAddress, virtualServers[serviceGroups' @@ -146,6 +149,7 @@ def delete_service(self, service_id): :param int service_id: The id of the service to delete """ + svc = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_Service'] @@ -156,6 +160,7 @@ def delete_service_group(self, group_id): :param int group_id: The id of the service group to delete """ + svc = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_VirtualServer'] @@ -166,6 +171,7 @@ def toggle_service_status(self, service_id): :param int service_id: The id of the service to delete """ + svc = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_Service'] return svc.toggleStatus(id=service_id) @@ -173,6 +179,7 @@ def toggle_service_status(self, service_id): def edit_service(self, loadbal_id, service_id, ip_address_id=None, port=None, enabled=None, hc_type=None, weight=None): """ Edits an existing service properties + :param int loadbal_id: The id of the loadbal where the service resides :param int service_id: The id of the service to edit :param string ip_address: The ip address of the service @@ -181,28 +188,26 @@ def edit_service(self, loadbal_id, service_id, ip_address_id=None, :param int hc_type: The health check type :param int weight: the weight to give to the service """ - _filter = utils.NestedDict({}) - _filter['virtualServers']['serviceGroups']['services']['id'] = ( - utils.query_filter(service_id)) - kwargs = utils.NestedDict({}) - kwargs['filter'] = _filter.to_dict() - kwargs['mask'] = ('mask[serviceGroups[services[groupReferences,' - 'healthChecks]]]') + _filter = {'virtualServers': {'serviceGroups': {'services': {'id': + utils.query_filter(service_id)}}}} + + mask = 'serviceGroups[services[groupReferences,healthChecks]]' virtual_servers = self.lb_svc.getVirtualServers(id=loadbal_id, - **kwargs) + filter=_filter, + mask=mask) + for service in virtual_servers[0]['serviceGroups'][0]['services']: if service['id'] == service_id: if enabled is not None: - service['enabled'] = int(enabled) + service['enabled'] = enabled if port is not None: - service['port'] = int(port) + service['port'] = port if weight is not None: - service['groupReferences'][0]['weight'] = int(weight) + service['groupReferences'][0]['weight'] = weight if hc_type is not None: - service['healthChecks'][0]['healthCheckTypeId'] = ( - int(hc_type)) + service['healthChecks'][0]['healthCheckTypeId'] = hc_type if ip_address_id is not None: service['ipAddressId'] = ip_address_id @@ -254,16 +259,14 @@ def add_service_group(self, lb_id, allocation=100, port=80, routing_type=2, routing_method=10): """ Adds a new service group to the load balancer :param int loadbal_id: The id of the loadbal where the service resides - :param int allocation: the % of connections to allocate to the group + :param int allocation: percent of connections to allocate toward the + group :param int port: the port of the service group :param int routing_type: the routing type to set on the service group :param int routing_method: The routing method to set on the group """ - kwargs = utils.NestedDict({}) - kwargs['mask'] = ('mask[virtualServers[serviceGroups' - '[services[groupReferences]]]]') - load_balancer = self.lb_svc.getObject(id=lb_id, **kwargs) - virtual_servers = load_balancer['virtualServers'] + mask = 'virtualServers[serviceGroups[services[groupReferences]]]' + load_balancer = self.lb_svc.getObject(id=lb_id, mask=mask) service_template = { 'port': port, 'allocation': allocation, @@ -275,7 +278,7 @@ def add_service_group(self, lb_id, allocation=100, port=80, ] } - virtual_servers.append(service_template) + load_balancer['virtualServers'].append(service_template) return self.lb_svc.editObject(load_balancer, id=lb_id) def edit_service_group(self, loadbal_id, group_id, allocation=None, @@ -298,13 +301,13 @@ def edit_service_group(self, loadbal_id, group_id, allocation=None, if virtual_server['id'] == group_id: service_group = virtual_server['serviceGroups'][0] if allocation is not None: - virtual_server['allocation'] = int(allocation) + virtual_server['allocation'] = allocation if port is not None: - virtual_server['port'] = int(port) + virtual_server['port'] = port if routing_type is not None: - service_group['routingTypeId'] = int(routing_type) + service_group['routingTypeId'] = routing_type if routing_method is not None: - service_group['routingMethodId'] = int(routing_method) + service_group['routingMethodId'] = routing_method break return self.lb_svc.editObject(load_balancer, id=loadbal_id) @@ -313,15 +316,11 @@ def reset_service_group(self, loadbal_id, group_id): :param int loadbal_id: The id of the loadbal :param int group_id: The id of the service group to reset """ - _filter = utils.NestedDict({}) - _filter['virtualServers']['id'] = utils.query_filter(group_id) - - kwargs = utils.NestedDict({}) - kwargs['filter'] = _filter.to_dict() - kwargs['mask'] = 'mask[serviceGroups]' + _filter = {'virtualServers': {'id': utils.query_filter(group_id)}} virtual_servers = self.lb_svc.getVirtualServers(id=loadbal_id, - **kwargs) + filter=_filter, + mask='serviceGroups') actual_id = virtual_servers[0]['serviceGroups'][0]['id'] svc = self.client['Network_Application_Delivery_Controller' diff --git a/SoftLayer/tests/managers/firewall_tests.py b/SoftLayer/tests/managers/firewall_tests.py index 93e367832..825707f04 100644 --- a/SoftLayer/tests/managers/firewall_tests.py +++ b/SoftLayer/tests/managers/firewall_tests.py @@ -68,7 +68,7 @@ def test_get_standard_package_virtual_server(self): } package_call.assert_called_once_with(filter=_filter, id=0) - mask = ('mask[primaryNetworkComponent[maxSpeed]]') + mask = 'primaryNetworkComponent[maxSpeed]' call2 = self.client['Virtual_Guest'].getObject call2.assert_called_once_with(id=1234, mask=mask) @@ -77,8 +77,7 @@ def test_get_standard_package_bare_metal(self): # we should ask for the frontEndNetworkComponents to get # the firewall port speed - mask = ('mask[id,maxSpeed,' - 'networkComponentGroup.networkComponents]') + mask = 'id,maxSpeed,networkComponentGroup.networkComponents' fenc_call = self.client['Hardware_Server'].getFrontendNetworkComponents fenc_call.assert_called_once_with(id=1234, mask=mask) @@ -155,7 +154,7 @@ def test_add_standard_firewall_cci(self): f.assert_called_once_with(filter=_filter, id=0) call2 = self.client['Virtual_Guest'].getObject - mask = ('mask[primaryNetworkComponent[maxSpeed]]') + mask = 'primaryNetworkComponent[maxSpeed]' call2.assert_called_once_with(id=server_id, mask=mask) f = self.client['Product_Order'].placeOrder f.assert_called_once() @@ -182,8 +181,7 @@ def test_add_standard_firewall_server(self): # we should ask for the frontEndNetworkComponents to get # the firewall port speed - mask = ('mask[id,maxSpeed,' - 'networkComponentGroup.networkComponents]') + mask = 'id,maxSpeed,networkComponentGroup.networkComponents' fenc_call = self.client['Hardware_Server'].getFrontendNetworkComponents fenc_call.assert_called_once_with(id=server_id, mask=mask) diff --git a/SoftLayer/tests/managers/loadbal_tests.py b/SoftLayer/tests/managers/loadbal_tests.py index 8c2cfc92e..6972579c9 100644 --- a/SoftLayer/tests/managers/loadbal_tests.py +++ b/SoftLayer/tests/managers/loadbal_tests.py @@ -45,12 +45,12 @@ def test_get_routing_methods(self): f.assert_called_once() def test_get_location(self): - id1 = self.lb_mgr.get_location('sjc01') + id1 = self.lb_mgr._get_location('sjc01') f = self.client['Location'].getDataCenters f.assert_called_once() self.assertEqual(id1, 168642) - id2 = self.lb_mgr.get_location('dal05') + id2 = self.lb_mgr._get_location('dal05') f = self.client['Location'].getDataCenters f.assert_called_once() self.assertEqual(id2, 'FIRST_AVAILABLE') @@ -151,7 +151,7 @@ def test_edit_service(self): } } } - mask = 'mask[serviceGroups[services[groupReferences,healthChecks]]]' + mask = 'serviceGroups[services[groupReferences,healthChecks]]' call.assert_called_once_with(filter=_filter, mask=mask, id=loadbal_id) call = self.client['Network_Application_Delivery_Controller_' @@ -213,8 +213,7 @@ def test_add_service_group(self): call = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_VirtualIpAddress'].getObject - mask = ('mask[virtualServers[serviceGroups' - '[services[groupReferences]]]]') + mask = 'virtualServers[serviceGroups[services[groupReferences]]]' call.assert_called_once_with(mask=mask, id=loadbal_id) call = self.client['Network_Application_Delivery_Controller_' @@ -229,8 +228,9 @@ def test_reset_service_group(self): 'LoadBalancer_VirtualIpAddress'].getVirtualServers _filter = {'virtualServers': {'id': {'operation': group_id}}} - mask = 'mask[serviceGroups]' - call.assert_called_once_with(filter=_filter, mask=mask, id=loadbal_id) + call.assert_called_once_with(filter=_filter, + mask='serviceGroups', + id=loadbal_id) call = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_Service_Group'].kickAllConnections From 73a0a15fad77a0b4369eca0a9010a1e9aa86f25f Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 17:40:42 -0500 Subject: [PATCH 0096/2592] More tilde and whitespace --- SoftLayer/managers/load_balancer.py | 30 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/SoftLayer/managers/load_balancer.py b/SoftLayer/managers/load_balancer.py index 7d441572f..f72af3227 100644 --- a/SoftLayer/managers/load_balancer.py +++ b/SoftLayer/managers/load_balancer.py @@ -1,6 +1,6 @@ """ SoftLayer.load_balancer - ~~~~~~~~~~~~~~~~~~ + ~~~~~~~~~~~~~~~~~~~~~~~ Load Balancer Manager/helpers :license: MIT, see LICENSE for more details. @@ -136,11 +136,11 @@ def get_local_lb(self, loadbal_id, **kwargs): """ if 'mask' not in kwargs: - kwargs['mask'] = ('mask[loadBalancerHardware[datacenter], ' + kwargs['mask'] = ('loadBalancerHardware[datacenter], ' 'ipAddress, virtualServers[serviceGroups' '[routingMethod,routingType,services' '[healthChecks[type], groupReferences,' - ' ipAddress]]]]') + ' ipAddress]]]') return self.lb_svc.getObject(id=loadbal_id, **kwargs) @@ -184,7 +184,7 @@ def edit_service(self, loadbal_id, service_id, ip_address_id=None, :param int service_id: The id of the service to edit :param string ip_address: The ip address of the service :param int port: the port of the service - :param int enabled: 1 to enable the service, 0 to disable it + :param bool enabled: enable or disable the search :param int hc_type: The health check type :param int weight: the weight to give to the service """ @@ -201,7 +201,7 @@ def edit_service(self, loadbal_id, service_id, ip_address_id=None, for service in virtual_servers[0]['serviceGroups'][0]['services']: if service['id'] == service_id: if enabled is not None: - service['enabled'] = enabled + service['enabled'] = int(enabled) if port is not None: service['port'] = port if weight is not None: @@ -217,13 +217,14 @@ def edit_service(self, loadbal_id, service_id, ip_address_id=None, return load_balancer def add_service(self, loadbal_id, service_group_id, ip_address_id, - port=80, enabled=1, hc_type=21, weight=1): + port=80, enabled=True, hc_type=21, weight=1): """ Adds a new service to the service group + :param int loadbal_id: The id of the loadbal where the service resides :param int service_group_id: The group to add the service to :param int ip_address id: The ip address ID of the service :param int port: the port of the service - :param int enabled: 1 to enable the service, 0 to disable it + :param bool enabled: Enable or disable the service :param int hc_type: The health check type :param int weight: the weight to give to the service """ @@ -236,7 +237,7 @@ def add_service(self, loadbal_id, service_group_id, ip_address_id, for virtual_server in virtual_servers: if virtual_server['id'] == service_group_id: service_template = { - 'enabled': enabled, + 'enabled': int(enabled), 'port': port, 'ipAddressId': ip_address_id, 'healthChecks': [ @@ -258,6 +259,7 @@ def add_service(self, loadbal_id, service_group_id, ip_address_id, def add_service_group(self, lb_id, allocation=100, port=80, routing_type=2, routing_method=10): """ Adds a new service group to the load balancer + :param int loadbal_id: The id of the loadbal where the service resides :param int allocation: percent of connections to allocate toward the group @@ -265,6 +267,7 @@ def add_service_group(self, lb_id, allocation=100, port=80, :param int routing_type: the routing type to set on the service group :param int routing_method: The routing method to set on the group """ + mask = 'virtualServers[serviceGroups[services[groupReferences]]]' load_balancer = self.lb_svc.getObject(id=lb_id, mask=mask) service_template = { @@ -284,6 +287,7 @@ def add_service_group(self, lb_id, allocation=100, port=80, def edit_service_group(self, loadbal_id, group_id, allocation=None, port=None, routing_type=None, routing_method=None): """ Edit an existing service group + :param int loadbal_id: The id of the loadbal where the service resides :param int group_id: The id of the service group :param int allocation: the % of connections to allocate to the group @@ -291,12 +295,12 @@ def edit_service_group(self, loadbal_id, group_id, allocation=None, :param int routing_type: the routing type to set on the service group :param int routing_method: The routing method to set on the group """ - kwargs = utils.NestedDict({}) - kwargs['mask'] = ('mask[virtualServers[serviceGroups' - '[services[groupReferences]]]]') - load_balancer = self.lb_svc.getObject(id=loadbal_id, **kwargs) + mask = 'virtualServers[serviceGroups[services[groupReferences]]]' + + load_balancer = self.lb_svc.getObject(id=loadbal_id, mask=mask) virtual_servers = load_balancer['virtualServers'] + for virtual_server in virtual_servers: if virtual_server['id'] == group_id: service_group = virtual_server['serviceGroups'][0] @@ -309,10 +313,12 @@ def edit_service_group(self, loadbal_id, group_id, allocation=None, if routing_method is not None: service_group['routingMethodId'] = routing_method break + return self.lb_svc.editObject(load_balancer, id=loadbal_id) def reset_service_group(self, loadbal_id, group_id): """ Resets all the connections on the service group + :param int loadbal_id: The id of the loadbal :param int group_id: The id of the service group to reset """ From 7d644fe1ae56b1b44b5e83b256c16e613335169d Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 18:02:43 -0500 Subject: [PATCH 0097/2592] Fixes tests --- SoftLayer/tests/managers/loadbal_tests.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/SoftLayer/tests/managers/loadbal_tests.py b/SoftLayer/tests/managers/loadbal_tests.py index 6972579c9..4142d5929 100644 --- a/SoftLayer/tests/managers/loadbal_tests.py +++ b/SoftLayer/tests/managers/loadbal_tests.py @@ -88,7 +88,7 @@ def test_add_local_lb(self): def test_get_local_lbs(self): self.lb_mgr.get_local_lbs() call = self.client['Account'].getAdcLoadBalancers - mask = ('mask[loadBalancerHardware[datacenter],ipAddress]') + mask = 'mask[loadBalancerHardware[datacenter],ipAddress]' call.assert_called_once_with(mask=mask) def test_get_local_lb(self): @@ -97,11 +97,11 @@ def test_get_local_lb(self): call = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_VirtualIpAddress'].getObject - mask = ('mask[loadBalancerHardware[datacenter], ' + mask = ('loadBalancerHardware[datacenter], ' 'ipAddress, virtualServers[serviceGroups' '[routingMethod,routingType,services' '[healthChecks[type], groupReferences,' - ' ipAddress]]]]') + ' ipAddress]]]') call.assert_called_once_with(id=lb_id, mask=mask) def test_delete_service(self): @@ -194,8 +194,7 @@ def test_edit_service_group(self): call = self.client['Network_Application_Delivery_Controller_' 'LoadBalancer_VirtualIpAddress'].getObject - mask = ('mask[virtualServers[serviceGroups' - '[services[groupReferences]]]]') + mask = 'virtualServers[serviceGroups[services[groupReferences]]]' call.assert_called_once_with(mask=mask, id=loadbal_id) call = self.client['Network_Application_Delivery_Controller_' From c194146a275ed64dc9a2ff395f59041706718a38 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 10 Oct 2014 18:25:40 -0500 Subject: [PATCH 0098/2592] Styzle --- SoftLayer/managers/load_balancer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/load_balancer.py b/SoftLayer/managers/load_balancer.py index f72af3227..0980658fe 100644 --- a/SoftLayer/managers/load_balancer.py +++ b/SoftLayer/managers/load_balancer.py @@ -189,8 +189,10 @@ def edit_service(self, loadbal_id, service_id, ip_address_id=None, :param int weight: the weight to give to the service """ - _filter = {'virtualServers': {'serviceGroups': {'services': {'id': - utils.query_filter(service_id)}}}} + _filter = { + 'virtualServers': { + 'serviceGroups': { + 'services': {'id': utils.query_filter(service_id)}}}} mask = 'serviceGroups[services[groupReferences,healthChecks]]' From f3e0b6f654e02e28510d818ed799e5ff7ced52ad Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 13 Oct 2014 16:43:42 -0500 Subject: [PATCH 0099/2592] Adds fast cpu provision as a package type for the HW manager --- SoftLayer/managers/hardware.py | 4 +++- SoftLayer/tests/managers/hardware_tests.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 2f8addd40..d832eeb99 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -212,7 +212,9 @@ def get_available_dedicated_server_packages(self): ordering_manager = self.ordering_manager mask = 'id,name,description,type,isActive' - package_types = ['BARE_METAL_CPU', 'BARE_METAL_CORE'] + package_types = ['BARE_METAL_CPU', + 'BARE_METAL_CORE', + 'BARE_METAL_CPU_FAST_PROVISION'] packages = ordering_manager.get_packages_of_type(package_types, mask) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 51fd22f08..d110154fa 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -305,7 +305,9 @@ def test_get_available_dedicated_server_packages(self): 'operation': 'in', 'options': [{ 'name': 'data', - 'value': ['BARE_METAL_CPU', 'BARE_METAL_CORE'] + 'value': ['BARE_METAL_CPU', + 'BARE_METAL_CORE', + 'BARE_METAL_CPU_FAST_PROVISION'] }] } } From f0c05b6e36b2da585a4ec80147efdcf432c4ffed Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 20 Oct 2014 13:51:53 -0500 Subject: [PATCH 0100/2592] Use travis-ci badge with explicit branch --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 220d19e3f..95ba04264 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ SoftLayer API Python Client =========================== -.. image:: https://api.travis-ci.org/softlayer/softlayer-python.png +.. image:: https://travis-ci.org/softlayer/softlayer-python.svg?branch=master :target: https://travis-ci.org/softlayer/softlayer-python .. image:: https://landscape.io/github/softlayer/softlayer-python/master/landscape.png From 1f7b9472d4379e7002dd4b3ebd0dffb64debe01e Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 21 Oct 2014 16:50:04 -0500 Subject: [PATCH 0101/2592] Revert Fast bare metal provision until the implementation is complete --- SoftLayer/managers/hardware.py | 3 +-- SoftLayer/tests/managers/hardware_tests.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index d832eeb99..caa586a8b 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -213,8 +213,7 @@ def get_available_dedicated_server_packages(self): mask = 'id,name,description,type,isActive' package_types = ['BARE_METAL_CPU', - 'BARE_METAL_CORE', - 'BARE_METAL_CPU_FAST_PROVISION'] + 'BARE_METAL_CORE'] packages = ordering_manager.get_packages_of_type(package_types, mask) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index d110154fa..a6be812ba 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -306,8 +306,7 @@ def test_get_available_dedicated_server_packages(self): 'options': [{ 'name': 'data', 'value': ['BARE_METAL_CPU', - 'BARE_METAL_CORE', - 'BARE_METAL_CPU_FAST_PROVISION'] + 'BARE_METAL_CORE'] }] } } From 8a78a1696891731097d55f219d53bb3d9cbf003c Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Thu, 23 Oct 2014 10:17:23 -0500 Subject: [PATCH 0102/2592] Adding .spec file for python-softlayer --- python-softlayer.spec | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 python-softlayer.spec diff --git a/python-softlayer.spec b/python-softlayer.spec new file mode 100644 index 000000000..b93faf289 --- /dev/null +++ b/python-softlayer.spec @@ -0,0 +1,47 @@ +# sitelib for noarch packages, sitearch for others (remove the unneeded one) +%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} +%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} + +Name: python-softlayer +Version: 773ab17 +Release: 1%{?dist} +Summary: softlayer python interface + +License: Softlayer +URL: https://github.com/softlayer/softlayer-python +Source0: python-softlayer-773ab17.tar.gz + +#BuildArch: +BuildRequires: python-devel +Requires: python-requests, python-docopt = 0.6.1, python-prettytable >= 0.7.0 +Requires: python-importlib, python-six >= 1.6.1 + +%description + + +%prep +%setup -q + + +%build +# Remove CFLAGS=... for noarch packages (unneeded) +CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build + + +%install +rm -rf $RPM_BUILD_ROOT +%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT + + +%files +%doc +/usr/bin/sl +# For noarch packages: sitelib +%{python_sitelib}/* +# For arch-specific packages: sitearch +#%{python_sitearch}/* + + +%changelog +* Wed Mar 19 2014 Andy Bakun 773ab17-1 +- initial packaging From bff00651c28344d034fcec92e1f9fcad3a8098b2 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Thu, 23 Oct 2014 12:00:04 -0500 Subject: [PATCH 0103/2592] added github url to the spec file --- python-softlayer.spec | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/python-softlayer.spec b/python-softlayer.spec index b93faf289..e3f78b802 100644 --- a/python-softlayer.spec +++ b/python-softlayer.spec @@ -1,18 +1,20 @@ # sitelib for noarch packages, sitearch for others (remove the unneeded one) %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} %{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} +%global commit master +%global shortcommit %(c=%{commit}; echo ${c:0:7}) -Name: python-softlayer -Version: 773ab17 +Name: softlayer-python +Version: %{commit} Release: 1%{?dist} Summary: softlayer python interface License: Softlayer URL: https://github.com/softlayer/softlayer-python -Source0: python-softlayer-773ab17.tar.gz +Source: https://github.com/softlayer/softlayer-python/archive/%{commit}/softlayer-python-%{commit}.tar.gz #BuildArch: -BuildRequires: python-devel +BuildRequires: python-devel, python-setuptools Requires: python-requests, python-docopt = 0.6.1, python-prettytable >= 0.7.0 Requires: python-importlib, python-six >= 1.6.1 @@ -43,5 +45,9 @@ rm -rf $RPM_BUILD_ROOT %changelog +* Thu Oct 23 2014 Christopher Gallo - master-2 +- Changed Source to a proper github url, added python-setuptool build + requirement + * Wed Mar 19 2014 Andy Bakun 773ab17-1 - initial packaging From 585c8ca9b8ec20918ded43380db4b69ab4034cb6 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Thu, 23 Oct 2014 12:59:32 -0500 Subject: [PATCH 0104/2592] renamed the spec file --- python-softlayer.spec => softlayer-python.spec | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename python-softlayer.spec => softlayer-python.spec (100%) diff --git a/python-softlayer.spec b/softlayer-python.spec similarity index 100% rename from python-softlayer.spec rename to softlayer-python.spec From 656305b24a4aee209a9a061ce011dde7e1b81a66 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 23 Oct 2014 14:26:05 -0500 Subject: [PATCH 0105/2592] Update CHANGELOG --- CHANGELOG | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 341db6be2..826acdc75 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,11 +4,13 @@ * CLI: More detail added to the `sl image detail` and `sl image list` commands + * CLI: Adds command to import DNS entries from BIND zone files + * CLI+API: Adds support for booting into rescue images for virtual servers and hardware * API: Adds ability to order virtual and hardare servers from a quote to the ordering manager - * CLI: Fixes bug with sl server list-chassis and sl server list-chassis + * CLI: Fixes bug with `sl server list-chassis` and `sl server list-chassis` * API: Restructure of the way custom authentication can be plugged in the API client From 3eafa595e7615200a2e5a3b0dd0bdfebcbe7f499 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 7 Oct 2014 08:46:19 -0500 Subject: [PATCH 0106/2592] Conversion to click This includes a mess of changes: * stricter option parsing (ex: global options are specified directly after sl) * no prefix-based option matching that doc-opt did * entrypoint-based loading * Several CLI cleanups: ** Remove help command ** Renames dns subcommands to include zone- and record- ** Renames --hourly and --billing for `sl vs create` and `sl hardware create` to --billing=hourly or --billing=monthly. ** Many other subtle changes --- SoftLayer/CLI/cdn/__init__.py | 2 + SoftLayer/CLI/cdn/detail.py | 32 + SoftLayer/CLI/cdn/list.py | 43 + SoftLayer/CLI/cdn/load.py | 18 + SoftLayer/CLI/cdn/origin_add.py | 24 + SoftLayer/CLI/cdn/origin_list.py | 28 + SoftLayer/CLI/cdn/origin_remove.py | 18 + SoftLayer/CLI/cdn/purge.py | 18 + SoftLayer/CLI/config/__init__.py | 36 + SoftLayer/CLI/config/setup.py | 142 ++ SoftLayer/CLI/config/show.py | 16 + SoftLayer/CLI/core.py | 425 +++--- SoftLayer/CLI/dns/__init__.py | 2 + SoftLayer/CLI/dns/record_add.py | 27 + SoftLayer/CLI/dns/record_edit.py | 28 + SoftLayer/CLI/dns/record_list.py | 49 + SoftLayer/CLI/dns/record_remove.py | 23 + SoftLayer/CLI/dns/zone_create.py | 17 + SoftLayer/CLI/dns/zone_delete.py | 25 + SoftLayer/CLI/dns/zone_import.py | 117 ++ SoftLayer/CLI/dns/zone_list.py | 30 + SoftLayer/CLI/dns/zone_print.py | 19 + SoftLayer/CLI/environment.py | 148 ++- SoftLayer/CLI/exceptions.py | 22 +- SoftLayer/CLI/firewall/__init__.py | 18 + SoftLayer/CLI/firewall/add.py | 54 + SoftLayer/CLI/firewall/cancel.py | 31 + SoftLayer/CLI/firewall/detail.py | 49 + SoftLayer/CLI/firewall/edit.py | 180 +++ SoftLayer/CLI/firewall/list.py | 85 ++ SoftLayer/CLI/globalip/__init__.py | 2 + SoftLayer/CLI/globalip/assign.py | 21 + SoftLayer/CLI/globalip/cancel.py | 26 + SoftLayer/CLI/globalip/create.py | 42 + SoftLayer/CLI/globalip/list.py | 50 + SoftLayer/CLI/globalip/unassign.py | 20 + SoftLayer/CLI/helpers.py | 10 - SoftLayer/CLI/image/__init__.py | 11 + SoftLayer/CLI/image/delete.py | 20 + SoftLayer/CLI/image/detail.py | 59 + SoftLayer/CLI/image/edit.py | 31 + SoftLayer/CLI/image/list.py | 55 + SoftLayer/CLI/iscsi/__init__.py | 1 + SoftLayer/CLI/iscsi/cancel.py | 30 + SoftLayer/CLI/iscsi/create.py | 23 + SoftLayer/CLI/iscsi/detail.py | 54 + SoftLayer/CLI/iscsi/list.py | 40 + SoftLayer/CLI/loadbal/__init__.py | 12 + SoftLayer/CLI/loadbal/cancel.py | 29 + SoftLayer/CLI/loadbal/create.py | 25 + SoftLayer/CLI/loadbal/create_options.py | 35 + SoftLayer/CLI/loadbal/detail.py | 85 ++ SoftLayer/CLI/loadbal/group_add.py | 41 + SoftLayer/CLI/loadbal/group_delete.py | 28 + SoftLayer/CLI/loadbal/group_edit.py | 41 + SoftLayer/CLI/loadbal/group_reset.py | 21 + SoftLayer/CLI/loadbal/health_checks.py | 26 + SoftLayer/CLI/loadbal/list.py | 49 + SoftLayer/CLI/loadbal/routing_methods.py | 25 + SoftLayer/CLI/loadbal/routing_types.py | 24 + SoftLayer/CLI/loadbal/service_add.py | 52 + SoftLayer/CLI/loadbal/service_delete.py | 28 + SoftLayer/CLI/loadbal/service_edit.py | 49 + SoftLayer/CLI/loadbal/service_toggle.py | 28 + SoftLayer/CLI/metadata.py | 67 + SoftLayer/CLI/modules/__init__.py | 15 - SoftLayer/CLI/modules/cdn.py | 188 --- SoftLayer/CLI/modules/config.py | 199 --- SoftLayer/CLI/modules/dns.py | 369 ------ SoftLayer/CLI/modules/filters.py | 32 - SoftLayer/CLI/modules/firewall.py | 424 ------ SoftLayer/CLI/modules/globalip.py | 170 --- SoftLayer/CLI/modules/help.py | 31 - SoftLayer/CLI/modules/image.py | 175 --- SoftLayer/CLI/modules/iscsi.py | 189 --- SoftLayer/CLI/modules/loadbal.py | 591 --------- SoftLayer/CLI/modules/messaging.py | 522 -------- SoftLayer/CLI/modules/metadata.py | 243 ---- SoftLayer/CLI/modules/nas.py | 50 - SoftLayer/CLI/modules/rwhois.py | 98 -- SoftLayer/CLI/modules/server.py | 1107 ---------------- SoftLayer/CLI/modules/snapshot.py | 152 --- SoftLayer/CLI/modules/sshkey.py | 164 --- SoftLayer/CLI/modules/ssl.py | 171 --- SoftLayer/CLI/modules/subnet.py | 283 ---- SoftLayer/CLI/modules/summary.py | 43 - SoftLayer/CLI/modules/ticket.py | 277 ---- SoftLayer/CLI/modules/vlan.py | 141 -- SoftLayer/CLI/modules/vs.py | 1161 ----------------- SoftLayer/CLI/mq/__init__.py | 57 + SoftLayer/CLI/mq/accounts_list.py | 28 + SoftLayer/CLI/mq/endpoints_list.py | 27 + SoftLayer/CLI/mq/ping.py | 25 + SoftLayer/CLI/mq/queue_add.py | 46 + SoftLayer/CLI/mq/queue_detail.py | 26 + SoftLayer/CLI/mq/queue_edit.py | 46 + SoftLayer/CLI/mq/queue_list.py | 34 + SoftLayer/CLI/mq/queue_pop.py | 41 + SoftLayer/CLI/mq/queue_push.py | 32 + SoftLayer/CLI/mq/queue_remove.py | 30 + SoftLayer/CLI/mq/topic_add.py | 47 + SoftLayer/CLI/mq/topic_detail.py | 30 + SoftLayer/CLI/mq/topic_list.py | 29 + SoftLayer/CLI/mq/topic_push.py | 34 + SoftLayer/CLI/mq/topic_remove.py | 25 + SoftLayer/CLI/mq/topic_subscribe.py | 46 + SoftLayer/CLI/mq/topic_unsubscribe.py | 25 + SoftLayer/CLI/nas/__init__.py | 1 + SoftLayer/CLI/nas/list.py | 39 + SoftLayer/CLI/routes.py | 182 +++ SoftLayer/CLI/rwhois/__init__.py | 1 + SoftLayer/CLI/rwhois/edit.py | 55 + SoftLayer/CLI/rwhois/show.py | 34 + SoftLayer/CLI/server/__init__.py | 209 +++ SoftLayer/CLI/server/cancel.py | 38 + SoftLayer/CLI/server/cancel_reasons.py | 25 + SoftLayer/CLI/server/create.py | 295 +++++ SoftLayer/CLI/server/create_options.py | 106 ++ SoftLayer/CLI/server/detail.py | 111 ++ SoftLayer/CLI/server/edit.py | 43 + SoftLayer/CLI/server/list.py | 78 ++ SoftLayer/CLI/server/list_chassis.py | 26 + SoftLayer/CLI/server/nic_edit.py | 24 + SoftLayer/CLI/server/power.py | 79 ++ SoftLayer/CLI/server/reload.py | 37 + SoftLayer/CLI/server/rescue.py | 28 + SoftLayer/CLI/snapshot/__init__.py | 1 + SoftLayer/CLI/snapshot/cancel.py | 22 + SoftLayer/CLI/snapshot/create.py | 21 + SoftLayer/CLI/snapshot/create_space.py | 23 + SoftLayer/CLI/snapshot/list.py | 37 + SoftLayer/CLI/snapshot/restore_volume.py | 23 + SoftLayer/CLI/sshkey/__init__.py | 1 + SoftLayer/CLI/sshkey/add.py | 32 + SoftLayer/CLI/sshkey/edit.py | 25 + SoftLayer/CLI/sshkey/list.py | 34 + SoftLayer/CLI/sshkey/print.py | 36 + SoftLayer/CLI/sshkey/remove.py | 24 + SoftLayer/CLI/ssl/__init__.py | 1 + SoftLayer/CLI/ssl/add.py | 42 + SoftLayer/CLI/ssl/download.py | 34 + SoftLayer/CLI/ssl/edit.py | 39 + SoftLayer/CLI/ssl/list.py | 41 + SoftLayer/CLI/ssl/remove.py | 21 + SoftLayer/CLI/subnet/__init__.py | 1 + SoftLayer/CLI/subnet/cancel.py | 26 + SoftLayer/CLI/subnet/create.py | 64 + SoftLayer/CLI/subnet/detail.py | 71 + SoftLayer/CLI/subnet/list.py | 67 + SoftLayer/CLI/subnet/lookup.py | 60 + SoftLayer/CLI/summary.py | 45 + SoftLayer/CLI/template.py | 29 +- SoftLayer/CLI/ticket/__init__.py | 43 + SoftLayer/CLI/ticket/create.py | 29 + SoftLayer/CLI/ticket/detail.py | 22 + SoftLayer/CLI/ticket/list.py | 39 + SoftLayer/CLI/ticket/subjects.py | 22 + SoftLayer/CLI/ticket/summary.py | 33 + SoftLayer/CLI/ticket/update.py | 26 + SoftLayer/CLI/virt/__init__.py | 2 + SoftLayer/CLI/virt/cancel.py | 25 + SoftLayer/CLI/virt/capture.py | 37 + SoftLayer/CLI/virt/create.py | 282 ++++ SoftLayer/CLI/virt/create_options.py | 112 ++ SoftLayer/CLI/virt/detail.py | 112 ++ SoftLayer/CLI/virt/dns.py | 103 ++ SoftLayer/CLI/virt/edit.py | 51 + SoftLayer/CLI/virt/list.py | 75 ++ SoftLayer/CLI/virt/network.py | 27 + SoftLayer/CLI/virt/power.py | 85 ++ SoftLayer/CLI/virt/ready.py | 25 + SoftLayer/CLI/virt/reload.py | 34 + SoftLayer/CLI/virt/upgrade.py | 53 + SoftLayer/CLI/vlan/__init__.py | 1 + SoftLayer/CLI/vlan/detail.py | 86 ++ SoftLayer/CLI/vlan/list.py | 55 + SoftLayer/managers/cci.py | 5 +- SoftLayer/managers/cdn.py | 19 +- SoftLayer/managers/dns.py | 49 +- SoftLayer/managers/firewall.py | 28 +- SoftLayer/managers/hardware.py | 59 +- SoftLayer/managers/image.py | 19 +- SoftLayer/managers/iscsi.py | 33 +- SoftLayer/managers/load_balancer.py | 45 +- SoftLayer/managers/messaging.py | 70 +- SoftLayer/managers/metadata.py | 15 +- SoftLayer/managers/network.py | 47 +- SoftLayer/managers/ordering.py | 27 +- SoftLayer/managers/sshkey.py | 15 +- SoftLayer/managers/ssl.py | 13 +- SoftLayer/managers/ticket.py | 15 +- SoftLayer/managers/vs.py | 73 +- SoftLayer/testing/__init__.py | 26 +- SoftLayer/testing/fixture_client.py | 49 +- SoftLayer/testing/fixtures/Account.py | 35 +- SoftLayer/tests/CLI/core_tests.py | 314 ++--- SoftLayer/tests/CLI/environment_tests.py | 41 +- SoftLayer/tests/CLI/helper_tests.py | 87 +- SoftLayer/tests/CLI/modules/cdn_tests.py | 66 +- SoftLayer/tests/CLI/modules/config_tests.py | 99 +- SoftLayer/tests/CLI/modules/dns_tests.py | 172 +-- SoftLayer/tests/CLI/modules/firewall_tests.py | 34 +- SoftLayer/tests/CLI/modules/globalip_tests.py | 51 +- SoftLayer/tests/CLI/modules/help_tests.py | 23 - SoftLayer/tests/CLI/modules/import_tests.py | 20 - SoftLayer/tests/CLI/modules/nas_tests.py | 17 +- SoftLayer/tests/CLI/modules/rwhois_tests.py | 89 +- SoftLayer/tests/CLI/modules/server_tests.py | 1000 ++++++-------- SoftLayer/tests/CLI/modules/sshkey_tests.py | 100 +- SoftLayer/tests/CLI/modules/summary_tests.py | 33 +- SoftLayer/tests/CLI/modules/vs_tests.py | 142 +- SoftLayer/tests/managers/cci_tests.py | 5 +- SoftLayer/tests/managers/loadbal_tests.py | 5 - SoftLayer/tests/managers/network_tests.py | 15 +- SoftLayer/tests/transport_tests.py | 2 +- docs/api/client.rst | 2 +- docs/cli.rst | 165 ++- docs/cli/vs.rst | 205 +-- docs/dev/cli.rst | 239 +--- docs/dev/index.rst | 14 +- docs/index.rst | 8 +- setup.cfg | 4 +- setup.py | 22 +- tools/requirements.txt | 4 +- tools/test-requirements.txt | 3 +- tox.ini | 4 +- 226 files changed, 8327 insertions(+), 9219 deletions(-) create mode 100644 SoftLayer/CLI/cdn/__init__.py create mode 100644 SoftLayer/CLI/cdn/detail.py create mode 100644 SoftLayer/CLI/cdn/list.py create mode 100644 SoftLayer/CLI/cdn/load.py create mode 100644 SoftLayer/CLI/cdn/origin_add.py create mode 100644 SoftLayer/CLI/cdn/origin_list.py create mode 100644 SoftLayer/CLI/cdn/origin_remove.py create mode 100644 SoftLayer/CLI/cdn/purge.py create mode 100644 SoftLayer/CLI/config/__init__.py create mode 100644 SoftLayer/CLI/config/setup.py create mode 100644 SoftLayer/CLI/config/show.py create mode 100644 SoftLayer/CLI/dns/__init__.py create mode 100644 SoftLayer/CLI/dns/record_add.py create mode 100644 SoftLayer/CLI/dns/record_edit.py create mode 100644 SoftLayer/CLI/dns/record_list.py create mode 100644 SoftLayer/CLI/dns/record_remove.py create mode 100644 SoftLayer/CLI/dns/zone_create.py create mode 100644 SoftLayer/CLI/dns/zone_delete.py create mode 100644 SoftLayer/CLI/dns/zone_import.py create mode 100644 SoftLayer/CLI/dns/zone_list.py create mode 100644 SoftLayer/CLI/dns/zone_print.py create mode 100644 SoftLayer/CLI/firewall/__init__.py create mode 100644 SoftLayer/CLI/firewall/add.py create mode 100644 SoftLayer/CLI/firewall/cancel.py create mode 100644 SoftLayer/CLI/firewall/detail.py create mode 100644 SoftLayer/CLI/firewall/edit.py create mode 100644 SoftLayer/CLI/firewall/list.py create mode 100644 SoftLayer/CLI/globalip/__init__.py create mode 100644 SoftLayer/CLI/globalip/assign.py create mode 100644 SoftLayer/CLI/globalip/cancel.py create mode 100644 SoftLayer/CLI/globalip/create.py create mode 100644 SoftLayer/CLI/globalip/list.py create mode 100644 SoftLayer/CLI/globalip/unassign.py create mode 100644 SoftLayer/CLI/image/__init__.py create mode 100644 SoftLayer/CLI/image/delete.py create mode 100644 SoftLayer/CLI/image/detail.py create mode 100644 SoftLayer/CLI/image/edit.py create mode 100644 SoftLayer/CLI/image/list.py create mode 100644 SoftLayer/CLI/iscsi/__init__.py create mode 100644 SoftLayer/CLI/iscsi/cancel.py create mode 100644 SoftLayer/CLI/iscsi/create.py create mode 100644 SoftLayer/CLI/iscsi/detail.py create mode 100644 SoftLayer/CLI/iscsi/list.py create mode 100644 SoftLayer/CLI/loadbal/__init__.py create mode 100644 SoftLayer/CLI/loadbal/cancel.py create mode 100644 SoftLayer/CLI/loadbal/create.py create mode 100644 SoftLayer/CLI/loadbal/create_options.py create mode 100644 SoftLayer/CLI/loadbal/detail.py create mode 100644 SoftLayer/CLI/loadbal/group_add.py create mode 100644 SoftLayer/CLI/loadbal/group_delete.py create mode 100644 SoftLayer/CLI/loadbal/group_edit.py create mode 100644 SoftLayer/CLI/loadbal/group_reset.py create mode 100644 SoftLayer/CLI/loadbal/health_checks.py create mode 100644 SoftLayer/CLI/loadbal/list.py create mode 100644 SoftLayer/CLI/loadbal/routing_methods.py create mode 100644 SoftLayer/CLI/loadbal/routing_types.py create mode 100644 SoftLayer/CLI/loadbal/service_add.py create mode 100644 SoftLayer/CLI/loadbal/service_delete.py create mode 100644 SoftLayer/CLI/loadbal/service_edit.py create mode 100644 SoftLayer/CLI/loadbal/service_toggle.py create mode 100644 SoftLayer/CLI/metadata.py delete mode 100644 SoftLayer/CLI/modules/__init__.py delete mode 100644 SoftLayer/CLI/modules/cdn.py delete mode 100644 SoftLayer/CLI/modules/config.py delete mode 100755 SoftLayer/CLI/modules/dns.py delete mode 100644 SoftLayer/CLI/modules/filters.py delete mode 100755 SoftLayer/CLI/modules/firewall.py delete mode 100644 SoftLayer/CLI/modules/globalip.py delete mode 100644 SoftLayer/CLI/modules/help.py delete mode 100644 SoftLayer/CLI/modules/image.py delete mode 100644 SoftLayer/CLI/modules/iscsi.py delete mode 100755 SoftLayer/CLI/modules/loadbal.py delete mode 100644 SoftLayer/CLI/modules/messaging.py delete mode 100644 SoftLayer/CLI/modules/metadata.py delete mode 100644 SoftLayer/CLI/modules/nas.py delete mode 100644 SoftLayer/CLI/modules/rwhois.py delete mode 100644 SoftLayer/CLI/modules/server.py delete mode 100644 SoftLayer/CLI/modules/snapshot.py delete mode 100644 SoftLayer/CLI/modules/sshkey.py delete mode 100755 SoftLayer/CLI/modules/ssl.py delete mode 100644 SoftLayer/CLI/modules/subnet.py delete mode 100644 SoftLayer/CLI/modules/summary.py delete mode 100644 SoftLayer/CLI/modules/ticket.py delete mode 100644 SoftLayer/CLI/modules/vlan.py delete mode 100755 SoftLayer/CLI/modules/vs.py create mode 100644 SoftLayer/CLI/mq/__init__.py create mode 100644 SoftLayer/CLI/mq/accounts_list.py create mode 100644 SoftLayer/CLI/mq/endpoints_list.py create mode 100644 SoftLayer/CLI/mq/ping.py create mode 100644 SoftLayer/CLI/mq/queue_add.py create mode 100644 SoftLayer/CLI/mq/queue_detail.py create mode 100644 SoftLayer/CLI/mq/queue_edit.py create mode 100644 SoftLayer/CLI/mq/queue_list.py create mode 100644 SoftLayer/CLI/mq/queue_pop.py create mode 100644 SoftLayer/CLI/mq/queue_push.py create mode 100644 SoftLayer/CLI/mq/queue_remove.py create mode 100644 SoftLayer/CLI/mq/topic_add.py create mode 100644 SoftLayer/CLI/mq/topic_detail.py create mode 100644 SoftLayer/CLI/mq/topic_list.py create mode 100644 SoftLayer/CLI/mq/topic_push.py create mode 100644 SoftLayer/CLI/mq/topic_remove.py create mode 100644 SoftLayer/CLI/mq/topic_subscribe.py create mode 100644 SoftLayer/CLI/mq/topic_unsubscribe.py create mode 100644 SoftLayer/CLI/nas/__init__.py create mode 100644 SoftLayer/CLI/nas/list.py create mode 100644 SoftLayer/CLI/routes.py create mode 100644 SoftLayer/CLI/rwhois/__init__.py create mode 100644 SoftLayer/CLI/rwhois/edit.py create mode 100644 SoftLayer/CLI/rwhois/show.py create mode 100644 SoftLayer/CLI/server/__init__.py create mode 100644 SoftLayer/CLI/server/cancel.py create mode 100644 SoftLayer/CLI/server/cancel_reasons.py create mode 100644 SoftLayer/CLI/server/create.py create mode 100644 SoftLayer/CLI/server/create_options.py create mode 100644 SoftLayer/CLI/server/detail.py create mode 100644 SoftLayer/CLI/server/edit.py create mode 100644 SoftLayer/CLI/server/list.py create mode 100644 SoftLayer/CLI/server/list_chassis.py create mode 100644 SoftLayer/CLI/server/nic_edit.py create mode 100644 SoftLayer/CLI/server/power.py create mode 100644 SoftLayer/CLI/server/reload.py create mode 100644 SoftLayer/CLI/server/rescue.py create mode 100644 SoftLayer/CLI/snapshot/__init__.py create mode 100644 SoftLayer/CLI/snapshot/cancel.py create mode 100644 SoftLayer/CLI/snapshot/create.py create mode 100644 SoftLayer/CLI/snapshot/create_space.py create mode 100644 SoftLayer/CLI/snapshot/list.py create mode 100644 SoftLayer/CLI/snapshot/restore_volume.py create mode 100644 SoftLayer/CLI/sshkey/__init__.py create mode 100644 SoftLayer/CLI/sshkey/add.py create mode 100644 SoftLayer/CLI/sshkey/edit.py create mode 100644 SoftLayer/CLI/sshkey/list.py create mode 100644 SoftLayer/CLI/sshkey/print.py create mode 100644 SoftLayer/CLI/sshkey/remove.py create mode 100644 SoftLayer/CLI/ssl/__init__.py create mode 100644 SoftLayer/CLI/ssl/add.py create mode 100644 SoftLayer/CLI/ssl/download.py create mode 100644 SoftLayer/CLI/ssl/edit.py create mode 100644 SoftLayer/CLI/ssl/list.py create mode 100644 SoftLayer/CLI/ssl/remove.py create mode 100644 SoftLayer/CLI/subnet/__init__.py create mode 100644 SoftLayer/CLI/subnet/cancel.py create mode 100644 SoftLayer/CLI/subnet/create.py create mode 100644 SoftLayer/CLI/subnet/detail.py create mode 100644 SoftLayer/CLI/subnet/list.py create mode 100644 SoftLayer/CLI/subnet/lookup.py create mode 100644 SoftLayer/CLI/summary.py create mode 100644 SoftLayer/CLI/ticket/__init__.py create mode 100644 SoftLayer/CLI/ticket/create.py create mode 100644 SoftLayer/CLI/ticket/detail.py create mode 100644 SoftLayer/CLI/ticket/list.py create mode 100644 SoftLayer/CLI/ticket/subjects.py create mode 100644 SoftLayer/CLI/ticket/summary.py create mode 100644 SoftLayer/CLI/ticket/update.py create mode 100644 SoftLayer/CLI/virt/__init__.py create mode 100644 SoftLayer/CLI/virt/cancel.py create mode 100644 SoftLayer/CLI/virt/capture.py create mode 100644 SoftLayer/CLI/virt/create.py create mode 100644 SoftLayer/CLI/virt/create_options.py create mode 100644 SoftLayer/CLI/virt/detail.py create mode 100644 SoftLayer/CLI/virt/dns.py create mode 100644 SoftLayer/CLI/virt/edit.py create mode 100644 SoftLayer/CLI/virt/list.py create mode 100644 SoftLayer/CLI/virt/network.py create mode 100644 SoftLayer/CLI/virt/power.py create mode 100644 SoftLayer/CLI/virt/ready.py create mode 100644 SoftLayer/CLI/virt/reload.py create mode 100644 SoftLayer/CLI/virt/upgrade.py create mode 100644 SoftLayer/CLI/vlan/__init__.py create mode 100644 SoftLayer/CLI/vlan/detail.py create mode 100644 SoftLayer/CLI/vlan/list.py delete mode 100644 SoftLayer/tests/CLI/modules/help_tests.py delete mode 100644 SoftLayer/tests/CLI/modules/import_tests.py diff --git a/SoftLayer/CLI/cdn/__init__.py b/SoftLayer/CLI/cdn/__init__.py new file mode 100644 index 000000000..678a299ea --- /dev/null +++ b/SoftLayer/CLI/cdn/__init__.py @@ -0,0 +1,2 @@ +"""Content Delivery Network.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/cdn/detail.py b/SoftLayer/CLI/cdn/detail.py new file mode 100644 index 000000000..64ccc0cbb --- /dev/null +++ b/SoftLayer/CLI/cdn/detail.py @@ -0,0 +1,32 @@ +"""Detail a CDN Account.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('account_id') +@environment.pass_env +def cli(env, account_id): + """Detail a CDN Account.""" + + manager = SoftLayer.CDNManager(env.client) + account = manager.get_account(account_id) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + table.add_row(['id', account['id']]) + table.add_row(['account_name', account['cdnAccountName']]) + table.add_row(['type', account['cdnSolutionName']]) + table.add_row(['status', account['status']['name']]) + table.add_row(['created', account['createDate']]) + table.add_row(['notes', + account.get('cdnAccountNote', formatting.blank())]) + + return table diff --git a/SoftLayer/CLI/cdn/list.py b/SoftLayer/CLI/cdn/list.py new file mode 100644 index 000000000..9cf1220bf --- /dev/null +++ b/SoftLayer/CLI/cdn/list.py @@ -0,0 +1,43 @@ +"""List CDN Accounts.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.option('--sortby', + help='Column to sort by', + type=click.Choice(['id', + 'datacenter', + 'host', + 'cores', + 'memory', + 'primary_ip', + 'backend_ip'])) +@environment.pass_env +def cli(env, sortby): + """List all CDN accounts.""" + + manager = SoftLayer.CDNManager(env.client) + accounts = manager.list_accounts() + + table = formatting.Table(['id', + 'account_name', + 'type', + 'created', + 'notes']) + for account in accounts: + table.add_row([ + account['id'], + account['cdnAccountName'], + account['cdnSolutionName'], + account['createDate'], + account.get('cdnAccountNote', formatting.blank()) + ]) + + table.sortby = sortby + return table diff --git a/SoftLayer/CLI/cdn/load.py b/SoftLayer/CLI/cdn/load.py new file mode 100644 index 000000000..5dc53ca2b --- /dev/null +++ b/SoftLayer/CLI/cdn/load.py @@ -0,0 +1,18 @@ +"""Cache one or more files on all edge nodes.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('account_id') +@click.argument('content_url', nargs=-1) +@environment.pass_env +def cli(env, account_id, content_url): + """Cache one or more files on all edge nodes.""" + + manager = SoftLayer.CDNManager(env.client) + manager.load_content(account_id, content_url) diff --git a/SoftLayer/CLI/cdn/origin_add.py b/SoftLayer/CLI/cdn/origin_add.py new file mode 100644 index 000000000..c8f5635b5 --- /dev/null +++ b/SoftLayer/CLI/cdn/origin_add.py @@ -0,0 +1,24 @@ +"""Create an origin pull mapping.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('account_id') +@click.argument('content_url') +@click.option('--type', + help='The media type for this mapping (http, flash, wm, ...)', + default='http') +@click.option('--cname', + help='An optional CNAME to attach to the mapping') +@environment.pass_env +def cli(env, account_id, content_url, type, cname): + """Create an origin pull mapping.""" + + manager = SoftLayer.CDNManager(env.client) + manager.add_origin(account_id, type, content_url, cname) diff --git a/SoftLayer/CLI/cdn/origin_list.py b/SoftLayer/CLI/cdn/origin_list.py new file mode 100644 index 000000000..3a7b7c841 --- /dev/null +++ b/SoftLayer/CLI/cdn/origin_list.py @@ -0,0 +1,28 @@ +"""List origin pull mappings.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('account_id') +@environment.pass_env +def cli(env, account_id): + """List origin pull mappings.""" + + manager = SoftLayer.CDNManager(env.client) + origins = manager.get_origins(account_id) + + table = formatting.Table(['id', 'media_type', 'cname', 'origin_url']) + + for origin in origins: + table.add_row([origin['id'], + origin['mediaType'], + origin.get('cname', formatting.blank()), + origin['originUrl']]) + + return table diff --git a/SoftLayer/CLI/cdn/origin_remove.py b/SoftLayer/CLI/cdn/origin_remove.py new file mode 100644 index 000000000..055a93bb2 --- /dev/null +++ b/SoftLayer/CLI/cdn/origin_remove.py @@ -0,0 +1,18 @@ +"""Remove an origin pull mapping.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('account_id') +@click.argument('origin_id') +@environment.pass_env +def cli(env, account_id, origin_id): + """Remove an origin pull mapping.""" + + manager = SoftLayer.CDNManager(env.client) + manager.remove_origin(account_id, origin_id) diff --git a/SoftLayer/CLI/cdn/purge.py b/SoftLayer/CLI/cdn/purge.py new file mode 100644 index 000000000..09d0810ae --- /dev/null +++ b/SoftLayer/CLI/cdn/purge.py @@ -0,0 +1,18 @@ +"""Purge cached files from all edge nodes.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('account_id') +@click.argument('content_url', nargs=-1) +@environment.pass_env +def cli(env, account_id, content_url): + """Purge cached files from all edge nodes.""" + + manager = SoftLayer.CDNManager(env.client) + manager.purge_content(account_id, content_url) diff --git a/SoftLayer/CLI/config/__init__.py b/SoftLayer/CLI/config/__init__.py new file mode 100644 index 000000000..8a66b089a --- /dev/null +++ b/SoftLayer/CLI/config/__init__.py @@ -0,0 +1,36 @@ +"""CLI configuration.""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer.CLI import formatting + + +def get_settings_from_client(client): + """Pull out settings from a SoftLayer.Client instance. + + :param client: SoftLayer.Client instance + """ + settings = { + 'username': '', + 'api_key': '', + 'timeout': client.timeout or '', + 'endpoint_url': client.endpoint_url, + } + try: + settings['username'] = client.auth.username + settings['api_key'] = client.auth.api_key + except AttributeError: + pass + + return settings + + +def config_table(settings): + """Returns a config table.""" + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + table.add_row(['Username', settings['username'] or 'not set']) + table.add_row(['API Key', settings['api_key'] or 'not set']) + table.add_row(['Endpoint URL', settings['endpoint_url'] or 'not set']) + table.add_row(['Timeout', settings['timeout'] or 'not set']) + return table diff --git a/SoftLayer/CLI/config/setup.py b/SoftLayer/CLI/config/setup.py new file mode 100644 index 000000000..5b8fa7306 --- /dev/null +++ b/SoftLayer/CLI/config/setup.py @@ -0,0 +1,142 @@ +"""Setup CLI configuration.""" +# :license: MIT, see LICENSE for more details. +import os.path + +import SoftLayer +from SoftLayer import auth +from SoftLayer.CLI import config +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer import utils + +import click + + +def get_api_key(client, username, secret, endpoint_url=None): + """Attempts API-Key and password auth to get an API key. + + This will also generate an API key if one doesn't exist + """ + + client.endpoint_url = endpoint_url + client.auth = None + # Try to use a client with username/api key + if len(secret) == 64: + try: + client.auth = auth.BasicAuthentication(username, secret) + client['Account'].getCurrentUser() + return secret + except SoftLayer.SoftLayerAPIError as ex: + if 'invalid api token' not in ex.faultString.lower(): + raise + else: + # Try to use a client with username/password + client.authenticate_with_password(username, secret) + + user_record = client['Account'].getCurrentUser( + mask='id, apiAuthenticationKeys') + api_keys = user_record['apiAuthenticationKeys'] + if len(api_keys) == 0: + return client['User_Customer'].addApiAuthenticationKey( + id=user_record['id']) + return api_keys[0]['authenticationKey'] + + +@click.command() +@environment.pass_env +def cli(env): + """Edit configuration.""" + + username, secret, endpoint_url, timeout = get_user_input(env) + + api_key = get_api_key(env.client, username, secret, + endpoint_url=endpoint_url) + + path = '~/.softlayer' + if env.config_file: + path = env.config_file + config_path = os.path.expanduser(path) + + env.out(env.fmt(config.config_table({'username': username, + 'api_key': api_key, + 'endpoint_url': endpoint_url, + 'timeout': timeout}))) + + if not formatting.confirm('Are you sure you want to write settings ' + 'to "%s"?' % config_path, default=True): + raise exceptions.CLIAbort('Aborted.') + + # Persist the config file. Read the target config file in before + # setting the values to avoid clobbering settings + parsed_config = utils.configparser.RawConfigParser() + parsed_config.read(config_path) + try: + parsed_config.add_section('softlayer') + except utils.configparser.DuplicateSectionError: + pass + + parsed_config.set('softlayer', 'username', username) + parsed_config.set('softlayer', 'api_key', api_key) + parsed_config.set('softlayer', 'endpoint_url', endpoint_url) + + config_fd = os.fdopen(os.open(config_path, + (os.O_WRONLY | os.O_CREAT | os.O_TRUNC), + 0o600), + 'w') + try: + parsed_config.write(config_fd) + finally: + config_fd.close() + + return "Configuration Updated Successfully" + + +def get_user_input(env): + """Ask for username, secret (api_key or password) and endpoint_url.""" + + defaults = config.get_settings_from_client(env.client.real_client) + timeout = defaults['timeout'] + + # Ask for username + for _ in range(3): + username = (env.input('Username [%s]: ' % defaults['username']) + or defaults['username']) + if username: + break + else: + raise exceptions.CLIAbort('Aborted after 3 attempts') + + # Ask for 'secret' which can be api_key or their password + for _ in range(3): + secret = (env.getpass('API Key or Password [%s]: ' + % defaults['api_key']) + or defaults['api_key']) + if secret: + break + else: + raise exceptions.CLIAbort('Aborted after 3 attempts') + + # Ask for which endpoint they want to use + for _ in range(3): + endpoint_type = env.input( + 'Endpoint (public|private|custom): ') + endpoint_type = endpoint_type.lower() + if not endpoint_type: + endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT + break + if endpoint_type == 'public': + endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT + break + elif endpoint_type == 'private': + endpoint_url = SoftLayer.API_PRIVATE_ENDPOINT + break + elif endpoint_type == 'custom': + endpoint_url = env.input( + 'Endpoint URL [%s]: ' % defaults['endpoint_url'] + ) or defaults['endpoint_url'] + break + else: + raise exceptions.CLIAbort('Aborted after 3 attempts') + + return username, secret, endpoint_url, timeout diff --git a/SoftLayer/CLI/config/show.py b/SoftLayer/CLI/config/show.py new file mode 100644 index 000000000..a3f7119af --- /dev/null +++ b/SoftLayer/CLI/config/show.py @@ -0,0 +1,16 @@ +"""Show current CLI configuration.""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer.CLI import config +from SoftLayer.CLI import environment + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """Show current configuration.""" + + settings = config.get_settings_from_client(env.client.real_client) + return config.config_table(settings) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index c47b24a54..2f4842e54 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -1,262 +1,233 @@ """ -usage: sl [...] - sl help - sl help - sl [-h | --help] - -SoftLayer Command-line Client {version} - -The available modules are: - -Compute: - image Manages compute and flex images - metadata Get details about this machine. Also available with 'my' and 'meta' - server Bare metal servers - sshkey Manage SSH keys on your account - vs Virtual Servers (formerly CCIs) - -Networking: - cdn Content Delivery Network service management - dns Domain Name System - firewall Firewall rule and security management - loadbal Load Balancer management - globalip Global IP address management - messaging Message Queue Service - rwhois RWhoIs operations - ssl Manages SSL - subnet Subnet ordering and management - vlan Manage VLANs on your account - -Storage: - iscsi View iSCSI details - nas View NAS details - snapshot iSCSI snapshots - -General: - config View and edit configuration for this tool - ticket Manage account tickets - summary Display an overall summary of your account - help Show help - -See 'sl help ' for more information on a specific module. - -To use most commands your SoftLayer username and api_key need to be configured. -The easiest way to do that is to use: 'sl config setup' -""" -# :license: MIT, see LICENSE for more details. - -# pylint: disable=W0703 + SoftLayer.core + ~~~~~~~~~~~~~~ + Core for the SoftLayer CLI + :license: MIT, see LICENSE for more details. +""" +from __future__ import print_function import logging import sys - -import docopt +import time +import types import SoftLayer from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting -from SoftLayer import consts +import click +# pylint: disable=too-many-public-methods, broad-except, unused-argument +# pylint: disable=redefined-builtin, super-init-not-called DEBUG_LOGGING_MAP = { - '0': logging.CRITICAL, - '1': logging.WARNING, - '2': logging.INFO, - '3': logging.DEBUG + 0: logging.CRITICAL, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG } -VALID_FORMATS = ['raw', 'table', 'json'] +VALID_FORMATS = ['table', 'raw', 'json'] +DEFAULT_FORMAT = 'raw' +if sys.stdout.isatty(): + DEFAULT_FORMAT = 'table' + + +class CommandLoader(click.MultiCommand): + """Loads commands for click.""" + def __init__(self, module=None, **attrs): + click.MultiCommand.__init__(self, **attrs) + self.module = module + + def list_commands(self, ctx): + """Get module for click.""" + env = ctx.ensure_object(environment.Environment) + return env.command_list(self.module) + + def get_command(self, ctx, name): + """Get command for click.""" + env = ctx.ensure_object(environment.Environment) + command = env.get_command(self.module, name) + return command + +class ModuleLoader(click.MultiCommand): + """Loads module for click.""" -def _append_common_options(arg_doc): - """Append common options to the doc string""" - default_format = 'raw' - if sys.stdout.isatty(): - default_format = 'table' + def list_commands(self, ctx): + """Get module for click.""" + env = ctx.ensure_object(environment.Environment) + return sorted(env.module_list()) - arg_doc += """ -Standard Options: - --format=ARG Output format. [Options: table, raw] [Default: %s] - -C FILE --config=FILE Config file location. [Default: ~/.softlayer] - --debug=LEVEL Specifies the debug noise level - 1=warn, 2=info, 3=debug - --timings Time each API call and display after results - --proxy=PROTO:PROXY_URL HTTP[s] proxy to be use to make API calls - -h --help Show this screen -""" % default_format - return arg_doc + def get_command(self, ctx, name): + """Get command for click.""" + env = ctx.ensure_object(environment.Environment) + # Do alias lookup + module_name = env.get_module_name(name) -class CommandParser(object): - """Helper class to parse commands. + module = env.get_module(module_name) + if isinstance(module, types.ModuleType): + return CommandLoader(module=module_name, help=module.__doc__) + else: + return module + + +class CliClient(SoftLayer.Client): + """A wrapped SoftLayer.Client that adds CLI-specific functionality. + + At the moment, it has a slightly different accounting for API call timings + but will also allow for 2FA and other similar functionality. - :param env: Environment instance """ - def __init__(self, env): - self.env = env - - def get_main_help(self): - """Get main help text.""" - main_doc = __doc__.format(version=SoftLayer.__version__) - return _append_common_options(main_doc).strip() - - def get_module_help(self, module_name): - """Get help text for a module.""" - module = self.env.load_module(module_name) - arg_doc = module.__doc__ - return _append_common_options(arg_doc).strip() - - def get_command_help(self, module_name, command_name): - """Get help text for a specific command.""" - command = self.env.get_command(module_name, command_name) - arg_doc = command.__doc__ - - if 'confirm' in command.options: - arg_doc += """ -Prompt Options: - -y, --really Confirm all prompt actions -""" - return _append_common_options(arg_doc).strip() - - def parse_main_args(self, args): - """Parse root arguments.""" - main_help = self.get_main_help() - arguments = docopt.docopt( - main_help, - version=consts.VERSION, - argv=args, - options_first=True) - arguments[''] = self.env.get_module_name(arguments['']) - return arguments - - def parse_module_args(self, module_name, args): - """Parse module arguments.""" - arg_doc = self.get_module_help(module_name) - arguments = docopt.docopt( - arg_doc, - version=consts.VERSION, - argv=[module_name] + args, - options_first=True) - return arguments - - def parse_command_args(self, module_name, command_name, args): - """Parse command arguments.""" - command = self.env.get_command(module_name, command_name) - arg_doc = self.get_command_help(module_name, command_name) - arguments = docopt.docopt(arg_doc, - version=consts.VERSION, - argv=[module_name] + args) - return command, arguments - - def parse(self, args): - """Parse entire tree of arguments.""" - # handle `sl ...` - main_args = self.parse_main_args(args) - module_name = main_args[''] - - # handle `sl ...` - module_args = self.parse_module_args(module_name, main_args['']) - - # get the command argument - command_name = module_args.get('') - - # handle `sl ...` - return self.parse_command_args( - module_name, - command_name, - main_args['']) - - -def main(args=sys.argv[1:], env=environment.Environment()): - """Entry point for the command-line client.""" - # Parse Top-Level Arguments + def __init__(self, client, *args, **kwargs): + self.real_client = client + self.last_calls = [] + # NOTE(kmcdonald): I really don't like this pattern. + + def call(self, service, method, *args, **kwargs): + """See Client.call for documentation.""" + start_time = time.time() + + result = self.real_client.call(service, method, *args, **kwargs) + + end_time = time.time() + diff = end_time - start_time + self.last_calls.append((service, method, start_time, diff)) + return result + + +@click.group(help="SoftLayer Command-line Client", + epilog="""To use most commands your SoftLayer +username and api_key need to be configured. The easiest way to do that is to +use: 'sl config setup'""", + cls=ModuleLoader) +@click.pass_context +@click.option('--format', + default=DEFAULT_FORMAT, + help="Output format", + type=click.Choice(VALID_FORMATS)) +@click.option('--config', '-C', + required=False, + default=click.get_app_dir('softlayer', + force_posix=True), + help="Config file location", + type=click.Path(resolve_path=True)) +@click.option('--debug', + required=False, + default='0', + help="Sets the debug noise level", + type=click.Choice(sorted([str(key) for key + in DEBUG_LOGGING_MAP.keys()]))) +@click.option('--verbose', '-v', + help="Sets the debug noise level", + type=click.IntRange(0, 3, clamp=True), + count=True) +@click.option('--timings', + required=False, + is_flag=True, + help="Time each API call and display after results") +@click.option('--proxy', + required=False, + help="HTTP[S] proxy to be use to make API calls") +@click.option('--really', '-y', + is_flag=True, + required=False, + help="Confirm all prompt actions") +@click.option('--fixtures', + is_flag=True, + required=False, + help="Use fixtures instead of actually making API calls") +@click.version_option(version=SoftLayer.__version__, + prog_name="SoftLayer Command-line Client") +def cli(ctx, + format='table', + config=None, + debug=0, + verbose=0, + proxy=None, + really=False, + fixtures=False, + **kwargs): + """Main click CLI entry-point.""" + + # Set logging level + debug_int = int(debug) + if debug_int: + verbose = debug_int + + if verbose: + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler()) + logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG)) + + # Populate environement with client and set it as the context object + env = ctx.ensure_object(environment.Environment) + env.skip_confirmations = really + env.config_file = config + env.format = format + if env.client is None: + # Environment can be passed in explicitly. This is used for testing + if fixtures: + from SoftLayer import testing + real_client = testing.FixtureClient() + else: + # Create SL Client + real_client = SoftLayer.Client(proxy=proxy, config_file=config) + + client = CliClient(real_client) + env.client = client + + +@cli.resultcallback() +@click.pass_context +def output_result(ctx, result, timings=False, **kwargs): + """Outputs the results returned by the CLI and also outputs timings.""" + + env = ctx.ensure_object(environment.Environment) + output = env.fmt(result) + if output: + env.out(output) + + if timings: + timing_table = formatting.Table(['service', 'method', 'time']) + + for service, call, _, duration in env.client.last_calls: + timing_table.add_row([service, call, duration]) + + env.err(env.fmt(timing_table)) + + +def main(): + """Main program. Catches several common errors and displays them nicely.""" exit_status = 0 - resolver = CommandParser(env) try: - command, command_args = resolver.parse(args) - - # Set logging level - debug_level = command_args.get('--debug') - if debug_level: - logger = logging.getLogger() - handler = logging.StreamHandler() - logger.addHandler(handler) - logger.setLevel(DEBUG_LOGGING_MAP.get(debug_level, logging.DEBUG)) - - kwargs = { - 'proxy': command_args.get('--proxy'), - 'config_file': command_args.get('--config') - } - if command_args.get('--timings'): - client = SoftLayer.TimedClient(**kwargs) - else: - client = SoftLayer.Client(**kwargs) - - # Do the thing - runnable = command(client=client, env=env) - data = runnable.execute(command_args) - if data: - out_format = command_args.get('--format', 'table') - if out_format not in VALID_FORMATS: - raise exceptions.ArgumentError('Invalid format "%s"' - % out_format) - output = formatting.format_output(data, fmt=out_format) - if output: - env.out(output) - - if command_args.get('--timings'): - out_format = command_args.get('--format', 'table') - api_calls = client.get_last_calls() - timing_table = formatting.KeyValueTable(['call', 'time']) - - for call, _, duration in api_calls: - timing_table.add_row([call, duration]) - - env.err(formatting.format_output(timing_table, fmt=out_format)) - - except exceptions.InvalidCommand as ex: - env.err(resolver.get_module_help(ex.module_name)) - if ex.command_name: - env.err('') - env.err(str(ex)) - exit_status = 1 - except exceptions.InvalidModule as ex: - env.err(resolver.get_main_help()) - if ex.module_name: - env.err('') - env.err(str(ex)) - exit_status = 1 - except docopt.DocoptExit as ex: - env.err(ex.usage) - env.err( - '\nUnknown argument(s), use -h or --help for available options') - exit_status = 127 - except KeyboardInterrupt: - env.out('') - exit_status = 1 - except exceptions.CLIAbort as ex: - env.err(str(ex.message)) - exit_status = ex.code - except SystemExit as ex: - exit_status = ex.code + cli.main() except SoftLayer.SoftLayerAPIError as ex: if 'invalid api token' in ex.faultString.lower(): - env.out("Authentication Failed: To update your credentials, use " - "'sl config setup'") + print("Authentication Failed: To update your credentials," + " use 'sl config setup'") + exit_status = 1 else: - env.err(str(ex)) + print(str(ex)) exit_status = 1 except SoftLayer.SoftLayerError as ex: - env.err(str(ex)) + print(str(ex)) exit_status = 1 + except exceptions.CLIAbort as ex: + print(str(ex.message)) + exit_status = ex.code except Exception: import traceback - env.err("An unexpected error has occured:") - env.err(traceback.format_exc()) - env.err("Feel free to report this error as it is likely a bug:") - env.err(" https://github.com/softlayer/softlayer-python/issues") + print("An unexpected error has occured:") + print(str(traceback.format_exc())) + print("Feel free to report this error as it is likely a bug:") + print(" https://github.com/softlayer/softlayer-python/issues") exit_status = 1 sys.exit(exit_status) + + +if __name__ == '__main__': + main() diff --git a/SoftLayer/CLI/dns/__init__.py b/SoftLayer/CLI/dns/__init__.py new file mode 100644 index 000000000..85833186f --- /dev/null +++ b/SoftLayer/CLI/dns/__init__.py @@ -0,0 +1,2 @@ +"""Domain Name System.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/dns/record_add.py b/SoftLayer/CLI/dns/record_add.py new file mode 100644 index 000000000..856c4be53 --- /dev/null +++ b/SoftLayer/CLI/dns/record_add.py @@ -0,0 +1,27 @@ +"""Add resource record.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('zone') +@click.argument('record') +@click.argument('type') +@click.argument('data') +@click.option('--ttl', + type=click.INT, + default=7200, + help='TTL value in seconds, such as 86400') +@environment.pass_env +def cli(env, zone, record, type, data, ttl): + """Add resource record.""" + + manager = SoftLayer.DNSManager(env.client) + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + manager.create_record(zone_id, record, type, data, ttl=ttl) diff --git a/SoftLayer/CLI/dns/record_edit.py b/SoftLayer/CLI/dns/record_edit.py new file mode 100644 index 000000000..f73e4c89f --- /dev/null +++ b/SoftLayer/CLI/dns/record_edit.py @@ -0,0 +1,28 @@ +"""Update DNS record.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('record_id') +@click.option('--record', help='Host record, such as www') +@click.option('--data', help='Record data, such as an IP address') +@click.option('--ttl', + type=click.INT, + help='TTL value in seconds, such as 86400') +@click.option('--type', help='Record type, such as A or CNAME') +@environment.pass_env +def cli(env, record_id, record, data, ttl, type): + """Update DNS record.""" + manager = SoftLayer.DNSManager(env.client) + result = manager.get_record(record_id) + result['host'] = record or result['record'] + result['ttl'] = ttl or result['ttl'] + result['type'] = type or result['type'] + result['data'] = data or result['data'] + manager.edit_record(result) diff --git a/SoftLayer/CLI/dns/record_list.py b/SoftLayer/CLI/dns/record_list.py new file mode 100644 index 000000000..bf66b9f59 --- /dev/null +++ b/SoftLayer/CLI/dns/record_list.py @@ -0,0 +1,49 @@ +"""List all records in a zone.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('zone') +@click.option('--data', help='Record data, such as an IP address') +@click.option('--record', help='Host record, such as www') +@click.option('--ttl', + type=click.INT, + help='TTL value in seconds, such as 86400') +@click.option('--type', help='Record type, such as A or CNAME') +@environment.pass_env +def cli(env, zone, data, record, ttl, type): + """List all records in a zone.""" + + manager = SoftLayer.DNSManager(env.client) + table = formatting.Table(['id', 'record', 'type', 'ttl', 'value']) + + table.align['ttl'] = 'l' + table.align['record'] = 'r' + table.align['value'] = 'l' + + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + + records = manager.get_records(zone_id, + record_type=type, + host=record, + ttl=ttl, + data=data) + + for record in records: + table.add_row([ + record['id'], + record['host'], + record['type'].upper(), + record['ttl'], + record['data'] + ]) + + return table diff --git a/SoftLayer/CLI/dns/record_remove.py b/SoftLayer/CLI/dns/record_remove.py new file mode 100644 index 000000000..3ef81c90b --- /dev/null +++ b/SoftLayer/CLI/dns/record_remove.py @@ -0,0 +1,23 @@ +"""Remove resource record.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('record_id') +@environment.pass_env +def cli(env, record_id): + """Add resource record.""" + + manager = SoftLayer.DNSManager(env.client) + + if env.skip_confirmations or formatting.no_going_back('yes'): + manager.delete_record(record_id) + else: + raise exceptions.CLIAbort("Aborted.") diff --git a/SoftLayer/CLI/dns/zone_create.py b/SoftLayer/CLI/dns/zone_create.py new file mode 100644 index 000000000..21b9e9289 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_create.py @@ -0,0 +1,17 @@ +"""Create a zone.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('zone') +@environment.pass_env +def cli(env, zone): + """Create a zone.""" + + manager = SoftLayer.DNSManager(env.client) + manager.create_zone(zone) diff --git a/SoftLayer/CLI/dns/zone_delete.py b/SoftLayer/CLI/dns/zone_delete.py new file mode 100644 index 000000000..8608043ef --- /dev/null +++ b/SoftLayer/CLI/dns/zone_delete.py @@ -0,0 +1,25 @@ +"""Delete zone.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('zone') +@environment.pass_env +def cli(env, zone): + """Delete zone.""" + + manager = SoftLayer.DNSManager(env.client) + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + + if env.skip_confirmations or formatting.no_going_back(zone): + manager.delete_zone(zone_id) + else: + raise exceptions.CLIAbort("Aborted.") diff --git a/SoftLayer/CLI/dns/zone_import.py b/SoftLayer/CLI/dns/zone_import.py new file mode 100644 index 000000000..505dab7d0 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_import.py @@ -0,0 +1,117 @@ +"""Import zone based off a BIND zone file.""" +# :license: MIT, see LICENSE for more details. +import re + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + +import click + +RECORD_REGEX = re.compile(r"""^((?P([\w-]+(\.)?)*|\@)?\s+ + (?P\d+)?\s+ + (?P\w+)?)?\s+ + (?P\w+)\s+ + (?P.*)""", re.X) +RECORD_FMT = "type={type}, record={record}, data={data}, ttl={ttl}" + + +@click.command() +@click.argument('zonefile', + type=click.Path(exists=True, readable=True, resolve_path=True)) +@click.option('--dry-run', is_flag=True, help="Don't actually create records") +@environment.pass_env +def cli(env, zonefile, dry_run): + """Import zone based off a BIND zone file.""" + + manager = SoftLayer.DNSManager(env.client) + with open(zonefile) as zone_f: + zone_contents = zone_f.read() + + zone, records, bad_lines = parse_zone_details(zone_contents) + + env.out("Parsed: zone=%s" % zone) + for record in records: + env.out("Parsed: %s" % RECORD_FMT.format(**record)) + + for line in bad_lines: + env.out("Unparsed: %s" % line) + + if dry_run: + return + + # Find zone id or create the zone if it doesn't exist + try: + zone_id = helpers.resolve_id(manager.resolve_ids, zone, + name='zone') + except exceptions.CLIAbort: + zone_id = manager.create_zone(zone)['id'] + env.out(click.style("Created: %s" % zone, fg='green')) + + # Attempt to create each record + for record in records: + try: + manager.create_record(zone_id, + record['record'], + record['type'], + record['data'], + record['ttl']) + + env.out(click.style("Created: %s" % RECORD_FMT.format(**record), + fg='green')) + except SoftLayer.SoftLayerAPIError as ex: + env.out(click.style("Failed: %s" % RECORD_FMT.format(**record), + fg='red')) + env.out(click.style(str(ex), fg='red')) + + env.out(click.style("Finished", fg='green')) + + +def parse_zone_details(zone_contents): + """Parses a zone file into python data-structures.""" + records = [] + bad_lines = [] + zone_lines = [line.strip() for line in zone_contents.split('\n')] + + zone_search = re.search(r'^\$ORIGIN (?P.*)\.', zone_lines[0]) + zone = zone_search.group('zone') + + for line in zone_lines[1:]: + record_search = re.search(RECORD_REGEX, line) + if record_search is None: + bad_lines.append(line) + continue + + name = record_search.group('domain') + # The API requires we send a host, although bind allows a blank + # entry. @ is the same thing as blank + if name is None: + name = "@" + + ttl = record_search.group('ttl') + # we don't do anything with the class + # domain_class = domainSearch.group('class') + record_type = record_search.group('type').upper() + data = record_search.group('data') + + # the dns class doesn't support weighted MX records yet, so we chomp + # that part out. + if record_type == "MX": + record_search = re.search(r'(?P\d+)\s+(?P.*)', data) + data = record_search.group('data') + + # This will skip the SOA record bit. And any domain that gets + # parsed oddly. + if record_type == 'IN': + bad_lines.append(line) + continue + + records.append({ + 'record': name, + 'type': record_type, + 'data': data, + 'ttl': ttl, + }) + + return zone, records, bad_lines diff --git a/SoftLayer/CLI/dns/zone_list.py b/SoftLayer/CLI/dns/zone_list.py new file mode 100644 index 000000000..8ad628af8 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_list.py @@ -0,0 +1,30 @@ +"""List all zones.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List all zones.""" + + manager = SoftLayer.DNSManager(env.client) + zones = manager.list_zones() + table = formatting.Table(['id', 'zone', 'serial', 'updated']) + table.align['serial'] = 'c' + table.align['updated'] = 'c' + + for zone in zones: + table.add_row([ + zone['id'], + zone['name'], + zone['serial'], + zone['updateDate'], + ]) + + return table diff --git a/SoftLayer/CLI/dns/zone_print.py b/SoftLayer/CLI/dns/zone_print.py new file mode 100644 index 000000000..fcb2ab0c2 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_print.py @@ -0,0 +1,19 @@ +"""Print zone in BIND format.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('zone') +@environment.pass_env +def cli(env, zone): + """Print zone in BIND format.""" + + manager = SoftLayer.DNSManager(env.client) + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + return manager.dump_zone(zone_id) diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 58a024c4c..58031e80d 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -7,22 +7,22 @@ """ import getpass import importlib -import inspect -import os -import os.path -import sys from SoftLayer.CLI import exceptions -from SoftLayer.CLI import modules +from SoftLayer.CLI import formatting +from SoftLayer.CLI import routes from SoftLayer import utils -# pylint: disable=R0201 +import click +import pkg_resources + +# pylint: disable=too-many-instance-attributes, invalid-name class Environment(object): """Provides access to the current CLI environment.""" def __init__(self): - # {'module_name': {'action': 'actionClass'}} + # {'module_name': {'action': plugin_loader}} self.plugins = {} self.aliases = { 'meta': 'metadata', @@ -35,58 +35,90 @@ def __init__(self): 'virtual': 'vs', 'lb': 'loadbal', } - self.stdout = sys.stdout - self.stderr = sys.stderr + self.client = None + self.format = 'table' + self.skip_confirmations = False + self._modules_loaded = False + self.config_file = None + + def command_list(self, module_name): + """Command listing.""" + + self._load_modules() + # Filter commands registered as None. These are the bases. + return sorted([m for m in self.plugins[module_name].keys() + if m is not None]) + + def module_list(self): + """Returns the list of modules in SoftLayer.CLI.modules.""" + self._load_modules() + return sorted(list(self.plugins.keys())) def get_command(self, module_name, command_name): """Based on the loaded modules, return a command.""" + self._load_modules() actions = self.plugins.get(module_name) or {} + if command_name in actions: - return actions[command_name] - if None in actions: - return actions[None] + return actions[command_name].load() + raise exceptions.InvalidCommand(module_name, command_name) + def get_module(self, module_name): + """Returns the module.""" + self._load_modules() + return self.get_command(module_name, None) + def get_module_name(self, module_name): """Returns the actual module name. Uses the alias mapping.""" if module_name in self.aliases: return self.aliases[module_name] return module_name - def load_module(self, module_name): # pragma: no cover - """Loads module by name.""" - try: - module = importlib.import_module('SoftLayer.CLI.modules.%s' - % module_name) - for _, obj in inspect.getmembers(module): - if inspect.isclass(obj) and issubclass(obj, CLIRunnable): - self.add_plugin(obj) - return module - except ImportError: - raise exceptions.InvalidModule(module_name) - - def add_plugin(self, cls): - """Add a CLIRunnable as a plugin to the environment.""" - command = cls.__module__.split('.')[-1] - if command not in self.plugins: - self.plugins[command] = {} - self.plugins[command][cls.action] = cls - - def plugin_list(self): - """Returns the list of modules in SoftLayer.CLI.modules.""" - return modules.get_module_list() + def _load_modules(self): + """Loads all modules.""" + if self._modules_loaded is True: + return + + self._load_modules_from_python() + self._load_modules_from_entry_points() + + self._modules_loaded = True + + def _load_modules_from_python(self): + """Load modules from the native python source.""" + for name, modpath in routes.ALL_ROUTES: + module, subcommand = _parse_name(name) + if module not in self.plugins: + self.plugins[module] = {} + + if ':' in modpath: + path, attr = modpath.split(':', 1) + else: + path, attr = modpath, None + self.plugins[module][subcommand] = ModuleLoader(path, attr=attr) + + def _load_modules_from_entry_points(self): + """Load modules from the entry_points (slower).""" + for obj in pkg_resources.iter_entry_points(group='softlayer.cli', + name=None): + + module, subcommand = _parse_name(obj.name) + if module not in self.plugins: + self.plugins[module] = {} + self.plugins[module][subcommand] = obj def out(self, output, newline=True): """Outputs a string to the console (stdout).""" - self.stdout.write(output) - if newline: - self.stdout.write(os.linesep) + click.echo(output, nl=newline) def err(self, output, newline=True): """Outputs an error string to the console (stderr).""" - self.stderr.write(output) - if newline: - self.stderr.write(os.linesep) + click.echo(output, nl=newline, err=True) + + def fmt(self, output): + """Format output based on current the environment format.""" + return formatting.format_output(output, fmt=self.format) def input(self, prompt): """Provide a command prompt.""" @@ -96,28 +128,30 @@ def getpass(self, prompt): """Provide a password prompt.""" return getpass.getpass(prompt) - def exit(self, code=0): - """Exit.""" - sys.exit(code) +class ModuleLoader(object): + """Module loader that acts a little like an EntryPoint object.""" -class CLIRunnable(object): - """This represents a descrete command or action in the CLI. + def __init__(self, import_path, attr=None): + self.import_path = import_path + self.attr = attr - CLIRunnable is intended to be subclassed. + def load(self): + """load and return the module/attribute.""" + module = importlib.import_module(self.import_path) + if self.attr: + return getattr(module, self.attr) + return module - """ - options = [] # set by subclass - action = 'not set' # set by subclass - def __init__(self, client=None, env=None): - self.client = client - self.env = env +def _parse_name(name): + """Parse command name and path from the given name.""" + if ':' in name: + module, subcommand = name.split(':', 1) + else: + module, subcommand = name, None - def execute(self, args): - """Execute the command. + return module, subcommand - This is intended to be overridden in a subclass. - """ - pass +pass_env = click.make_pass_decorator(Environment, ensure=True) diff --git a/SoftLayer/CLI/exceptions.py b/SoftLayer/CLI/exceptions.py index b1c725ef4..ea93462a4 100644 --- a/SoftLayer/CLI/exceptions.py +++ b/SoftLayer/CLI/exceptions.py @@ -15,6 +15,12 @@ def __init__(self, code=0, *args): super(CLIHalt, self).__init__(*args) self.code = code + def __str__(self): + return "" % (self.code, + getattr(self, 'message')) + + __repr__ = __str__ + class CLIAbort(CLIHalt): """Halt the execution of the command. Gives an exit code of 2.""" @@ -35,13 +41,9 @@ class InvalidCommand(SoftLayer.SoftLayerError): def __init__(self, module_name, command_name, *args): self.module_name = module_name self.command_name = command_name - error = 'Invalid command: "%s".' % self.command_name - SoftLayer.SoftLayerError.__init__(self, error, *args) - - -class InvalidModule(SoftLayer.SoftLayerError): - """Raised when trying to use a module that does not exist.""" - def __init__(self, module_name, *args): - self.module_name = module_name - error = 'Invalid module: "%s".' % self.module_name - SoftLayer.SoftLayerError.__init__(self, error, *args) + cmd_str = module_name + if command_name is not None: + cmd_str = '%s %s' % (module_name, command_name) + SoftLayer.SoftLayerError.__init__(self, + 'Invalid command: "%s"' % cmd_str, + *args) diff --git a/SoftLayer/CLI/firewall/__init__.py b/SoftLayer/CLI/firewall/__init__.py new file mode 100644 index 000000000..70b92df9f --- /dev/null +++ b/SoftLayer/CLI/firewall/__init__.py @@ -0,0 +1,18 @@ +"""Firewalls.""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer.CLI import exceptions + + +def parse_id(input_id): + """Helper package to retrieve the actual IDs. + + :param input_id: the ID provided by the user + :returns: A list of valid IDs + """ + key_value = input_id.split(':') + + if len(key_value) != 2: + raise exceptions.CLIAbort( + 'Invalid ID %s: ID should be of the form xxx:yyy' % input_id) + return key_value[0], int(key_value[1]) diff --git a/SoftLayer/CLI/firewall/add.py b/SoftLayer/CLI/firewall/add.py new file mode 100644 index 000000000..5a6a7d07a --- /dev/null +++ b/SoftLayer/CLI/firewall/add.py @@ -0,0 +1,54 @@ +"""Create new firewall.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('target') +@click.option('--firewall-type', + type=click.Choice(['vs', 'vlan', 'server']), + help='Firewall type', + required=True) +@click.option('--high-availability', '--ha', + is_flag=True, + help='High available firewall option') +@environment.pass_env +def cli(env, target, firewall_type, high_availability): + """Create new firewall.""" + + mgr = SoftLayer.FirewallManager(env.client) + + if not env.skip_confirmations: + if firewall_type == 'vlan': + pkg = mgr.get_dedicated_package(ha_enabled=high_availability) + elif firewall_type == 'vs': + pkg = mgr.get_standard_package(target, is_cci=True) + elif firewall_type == 'server': + pkg = mgr.get_standard_package(target, is_cci=False) + + if not pkg: + return "Unable to add firewall - Is network public enabled?" + + env.out("******************") + env.out("Product: %s" % pkg[0]['description']) + env.out("Price: $%s monthly" % pkg[0]['prices'][0]['recurringFee']) + env.out("******************") + + if not formatting.confirm("This action will incur charges on your " + "account. Continue?"): + raise exceptions.CLIAbort('Aborted.') + + if firewall_type == 'vlan': + mgr.add_vlan_firewall(target, ha_enabled=high_availability) + elif firewall_type == 'vs': + mgr.add_standard_firewall(target, is_cci=True) + elif firewall_type == 'server': + mgr.add_standard_firewall(target, is_cci=False) + + return "Firewall is being created!" diff --git a/SoftLayer/CLI/firewall/cancel.py b/SoftLayer/CLI/firewall/cancel.py new file mode 100644 index 000000000..c15670eb8 --- /dev/null +++ b/SoftLayer/CLI/firewall/cancel.py @@ -0,0 +1,31 @@ +"""List firewalls.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import firewall +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """List firewalls.""" + + mgr = SoftLayer.FirewallManager(env.client) + firewall_type, firewall_id = firewall.parse_id(identifier) + + if any([env.skip_confirmations, + formatting.confirm("This action will cancel a firewall from your" + "account. Continue?")]): + if firewall_type in ['cci', 'server']: + mgr.cancel_firewall(firewall_id, dedicated=False) + elif firewall_type == 'vlan': + mgr.cancel_firewall(firewall_id, dedicated=True) + return 'Firewall with id %s is being cancelled!' % identifier + else: + raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/firewall/detail.py b/SoftLayer/CLI/firewall/detail.py new file mode 100644 index 000000000..4ba53dc62 --- /dev/null +++ b/SoftLayer/CLI/firewall/detail.py @@ -0,0 +1,49 @@ +"""Detail firewall.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import firewall +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Detail firewall.""" + + mgr = SoftLayer.FirewallManager(env.client) + + firewall_type, firewall_id = firewall.parse_id(identifier) + if firewall_type == 'vlan': + rules = mgr.get_dedicated_fwl_rules(firewall_id) + else: + rules = mgr.get_standard_fwl_rules(firewall_id) + + return get_rules_table(rules) + + +def get_rules_table(rules): + """Helper to format the rules into a table. + + :param list rules: A list containing the rules of the firewall + :returns: a formatted table of the firewall rules + """ + table = formatting.Table(['#', 'action', 'protocol', 'src_ip', 'src_mask', + 'dest', 'dest_mask']) + table.sortby = '#' + for rule in rules: + table.add_row([ + rule['orderValue'], + rule['action'], + rule['protocol'], + rule['sourceIpAddress'], + rule['sourceIpSubnetMask'], + '%s:%s-%s' % (rule['destinationIpAddress'], + rule['destinationPortRangeStart'], + rule['destinationPortRangeEnd']), + rule['destinationIpSubnetMask']]) + return table diff --git a/SoftLayer/CLI/firewall/edit.py b/SoftLayer/CLI/firewall/edit.py new file mode 100644 index 000000000..0ea61e330 --- /dev/null +++ b/SoftLayer/CLI/firewall/edit.py @@ -0,0 +1,180 @@ +"""List firewalls.""" +# :license: MIT, see LICENSE for more details. + +import os +import subprocess +import tempfile + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import firewall +from SoftLayer.CLI import formatting + +import click + +DELIMITER = "=========================================\n" + + +def parse_rules(content=None): + """Helper to parse the input from the user into a list of rules. + + :param string content: the content of the editor + :returns: a list of rules + """ + rules = content.split(DELIMITER) + parsed_rules = list() + order = 1 + for rule in rules: + if rule.strip() == '': + continue + parsed_rule = {} + lines = rule.split("\n") + parsed_rule['orderValue'] = order + order += 1 + for line in lines: + if line.strip() == '': + continue + key_value = line.strip().split(':') + key = key_value[0].strip() + value = key_value[1].strip() + if key == 'action': + parsed_rule['action'] = value + elif key == 'protocol': + parsed_rule['protocol'] = value + elif key == 'source_ip_address': + parsed_rule['sourceIpAddress'] = value + elif key == 'source_ip_subnet_mask': + parsed_rule['sourceIpSubnetMask'] = value + elif key == 'destination_ip_address': + parsed_rule['destinationIpAddress'] = value + elif key == 'destination_ip_subnet_mask': + parsed_rule['destinationIpSubnetMask'] = value + elif key == 'destination_port_range_start': + parsed_rule['destinationPortRangeStart'] = int(value) + elif key == 'destination_port_range_end': + parsed_rule['destinationPortRangeEnd'] = int(value) + elif key == 'version': + parsed_rule['version'] = int(value) + parsed_rules.append(parsed_rule) + return parsed_rules + + +def open_editor(rules=None, content=None): + """Helper to open an editor for editing the firewall rules. + + This method takes two parameters, if content is provided, + that means that submitting the rules failed and we are allowing + the user to re-edit what they provided. If content is not provided, the + rules retrieved from the firewall will be displayed to the user. + + :param list rules: A list containing the rules of the firewall + :param string content: the content that the user provided in the editor + :returns: a formatted string that get be pushed into the editor + """ + + # Let's get the default EDITOR of the environment, + # use nano if none is specified + editor = os.environ.get('EDITOR', 'nano') + + with tempfile.NamedTemporaryFile(suffix=".tmp") as tfile: + + if content: + # if content is provided, just display it as is + tfile.write(content) + tfile.flush() + subprocess.call([editor, tfile.name]) + tfile.seek(0) + data = tfile.read() + return data + + if not rules: + # if the firewall has no rules, provide a template + tfile.write(DELIMITER) + tfile.write(get_formatted_rule()) + else: + # if the firewall has rules, display those to the user + for rule in rules: + tfile.write(DELIMITER) + tfile.write(get_formatted_rule(rule)) + tfile.write(DELIMITER) + tfile.flush() + subprocess.call([editor, tfile.name]) + tfile.seek(0) + data = tfile.read() + return data + + return + + +def get_formatted_rule(rule=None): + """Helper to format the rule into a user friendly format. + + :param dict rule: A dict containing one rule of the firewall + :returns: a formatted string that get be pushed into the editor + """ + rule = rule or {} + return ('action: %s\n' + 'protocol: %s\n' + 'source_ip_address: %s\n' + 'source_ip_subnet_mask: %s\n' + 'destination_ip_address: %s\n' + 'destination_ip_subnet_mask: %s\n' + 'destination_port_range_start: %s\n' + 'destination_port_range_end: %s\n' + 'version: %s\n' + % (rule.get('action', 'permit'), + rule.get('protocol', 'tcp'), + rule.get('sourceIpAddress', 'any'), + rule.get('sourceIpSubnetMask', '255.255.255.255'), + rule.get('destinationIpAddress', 'any'), + rule.get('destinationIpSubnetMask', '255.255.255.255'), + rule.get('destinationPortRangeStart', 1), + rule.get('destinationPortRangeEnd', 1), + rule.get('version', 4))) + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Edit firewall rules.""" + + mgr = SoftLayer.FirewallManager(env.client) + + firewall_type, firewall_id = firewall.parse_id(identifier) + if firewall_type == 'vlan': + orig_rules = mgr.get_dedicated_fwl_rules(firewall_id) + else: + orig_rules = mgr.get_standard_fwl_rules(firewall_id) + # open an editor for the user to enter their rules + edited_rules = open_editor(rules=orig_rules) + env.out(edited_rules) + if formatting.confirm("Would you like to submit the rules. " + "Continue?"): + while True: + try: + rules = parse_rules(edited_rules) + if firewall_type == 'vlan': + rules = mgr.edit_dedicated_fwl_rules(firewall_id, + rules) + else: + rules = mgr.edit_standard_fwl_rules(firewall_id, + rules) + break + except (SoftLayer.SoftLayerError, ValueError) as error: + env.out("Unexpected error({%s})" % (error)) + if formatting.confirm("Would you like to continue editing " + "the rules. Continue?"): + edited_rules = open_editor(content=edited_rules) + env.out(edited_rules) + if formatting.confirm("Would you like to submit the " + "rules. Continue?"): + continue + else: + raise exceptions.CLIAbort('Aborted.') + else: + raise exceptions.CLIAbort('Aborted.') + return 'Firewall updated!' + else: + raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/firewall/list.py b/SoftLayer/CLI/firewall/list.py new file mode 100644 index 000000000..9d0a17a8f --- /dev/null +++ b/SoftLayer/CLI/firewall/list.py @@ -0,0 +1,85 @@ +"""List firewalls.""" +# :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() +@environment.pass_env +def cli(env): + """List firewalls.""" + + mgr = SoftLayer.FirewallManager(env.client) + table = formatting.Table(['firewall id', + 'type', + 'features', + 'server/vlan id']) + fwvlans = mgr.get_firewalls() + dedicated_firewalls = [firewall for firewall in fwvlans + if firewall['dedicatedFirewallFlag']] + + for vlan in dedicated_firewalls: + features = [] + if vlan['highAvailabilityFirewallFlag']: + features.append('HA') + + if features: + feature_list = formatting.listing(features, separator=',') + else: + feature_list = formatting.blank() + + table.add_row([ + 'vlan:%s' % vlan['networkVlanFirewall']['id'], + 'VLAN - dedicated', + feature_list, + vlan['id'] + ]) + + shared_vlan = [firewall for firewall in fwvlans + if not firewall['dedicatedFirewallFlag']] + for vlan in shared_vlan: + vs_firewalls = [guest + for guest in vlan['firewallGuestNetworkComponents'] + if has_firewall_component(guest)] + + for firewall in vs_firewalls: + table.add_row([ + 'cci:%s' % firewall['id'], + 'CCI - standard', + '-', + firewall['guestNetworkComponent']['guest']['id'] + ]) + + server_firewalls = [server + for server in vlan['firewallNetworkComponents'] + if has_firewall_component(server)] + + for firewall in server_firewalls: + table.add_row([ + 'server:%s' % firewall['id'], + 'Server - standard', + '-', + utils.lookup(firewall, + 'networkComponent', + 'downlinkComponent', + 'hardwareId') + ]) + + return table + + +def has_firewall_component(server): + """Helper to determine whether or not a server has a firewall. + + :param dict server: A dictionary representing a server + :returns: True if the Server has a firewall. + """ + if server['status'] != 'no_edit': + return True + + return False diff --git a/SoftLayer/CLI/globalip/__init__.py b/SoftLayer/CLI/globalip/__init__.py new file mode 100644 index 000000000..68fb2f949 --- /dev/null +++ b/SoftLayer/CLI/globalip/__init__.py @@ -0,0 +1,2 @@ +"""Global IP addresses.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/globalip/assign.py b/SoftLayer/CLI/globalip/assign.py new file mode 100644 index 000000000..ba13739af --- /dev/null +++ b/SoftLayer/CLI/globalip/assign.py @@ -0,0 +1,21 @@ +"""Assigns the global IP to a target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@click.argument('target') +@environment.pass_env +def cli(env, identifier, target): + """Assigns the global IP to a target.""" + + mgr = SoftLayer.NetworkManager(env.client) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, + name='global ip') + mgr.assign_global_ip(global_ip_id, target) diff --git a/SoftLayer/CLI/globalip/cancel.py b/SoftLayer/CLI/globalip/cancel.py new file mode 100644 index 000000000..066088223 --- /dev/null +++ b/SoftLayer/CLI/globalip/cancel.py @@ -0,0 +1,26 @@ +"""Cancel global IP.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Cancel global IP.""" + + mgr = SoftLayer.NetworkManager(env.client) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, + name='global ip') + + if env.skip_confirmations or formatting.no_going_back(global_ip_id): + mgr.cancel_global_ip(global_ip_id) + else: + raise exceptions.CLIAbort('Aborted') diff --git a/SoftLayer/CLI/globalip/create.py b/SoftLayer/CLI/globalip/create.py new file mode 100644 index 000000000..4d0e62ffc --- /dev/null +++ b/SoftLayer/CLI/globalip/create.py @@ -0,0 +1,42 @@ +"""Creates a global IP.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.option('--ipv6', '--v6', is_flag=True, help='Order a IPv6 IP') +@click.option('--test', help='test order') +@environment.pass_env +def cli(env, ipv6, test): + """Creates a global IP.""" + + mgr = SoftLayer.NetworkManager(env.client) + + version = 4 + if ipv6: + version = 6 + if not test and not env.skip_confirmations: + if not formatting.confirm("This action will incur charges on your " + "account. Continue?"): + raise exceptions.CLIAbort('Cancelling order.') + result = mgr.add_global_ip(version=version, test_order=test) + + table = formatting.Table(['item', 'cost']) + table.align['Item'] = 'r' + table.align['cost'] = 'r' + + total = 0.0 + for price in result['orderDetails']['prices']: + total += float(price.get('recurringFee', 0.0)) + rate = "%.2f" % float(price['recurringFee']) + + table.add_row([price['item']['description'], rate]) + + table.add_row(['Total monthly cost', "%.2f" % total]) + return table diff --git a/SoftLayer/CLI/globalip/list.py b/SoftLayer/CLI/globalip/list.py new file mode 100644 index 000000000..d1a86bc08 --- /dev/null +++ b/SoftLayer/CLI/globalip/list.py @@ -0,0 +1,50 @@ +"""List all global IPs.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.option('--ip-version', + help='Display only IPv4', + type=click.Choice(['v4', 'v6'])) +@environment.pass_env +def cli(env, ip_version): + """List all global IPs.""" + + mgr = SoftLayer.NetworkManager(env.client) + + table = formatting.Table(['id', 'ip', 'assigned', 'target']) + + version = None + if ip_version == 'v4': + version = 4 + elif ip_version == 'v6': + version = 6 + + ips = mgr.list_global_ips(version=version) + + for ip_address in ips: + assigned = 'No' + target = 'None' + if ip_address.get('destinationIpAddress'): + dest = ip_address['destinationIpAddress'] + assigned = 'Yes' + target = dest['ipAddress'] + virtual_guest = dest.get('virtualGuest') + if virtual_guest: + target += (' (%s)' + % virtual_guest['fullyQualifiedDomainName']) + elif ip_address['destinationIpAddress'].get('hardware'): + target += (' (%s)' + % dest['hardware']['fullyQualifiedDomainName']) + + table.add_row([ip_address['id'], + ip_address['ipAddress']['ipAddress'], + assigned, + target]) + return table diff --git a/SoftLayer/CLI/globalip/unassign.py b/SoftLayer/CLI/globalip/unassign.py new file mode 100644 index 000000000..e7c0d9f7e --- /dev/null +++ b/SoftLayer/CLI/globalip/unassign.py @@ -0,0 +1,20 @@ +"""Unassigns a global IP from a target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Unassigns a global IP from a target.""" + + mgr = SoftLayer.NetworkManager(env.client) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, + name='global ip') + mgr.unassign_global_ip(global_ip_id) diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index 60d2852e9..135e36052 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -30,13 +30,3 @@ def resolve_id(resolver, identifier, name='object'): (name, identifier, ', '.join([str(_id) for _id in ids]))) return ids[0] - - -def sanitize_args(args): - """ sanitize input (remove = sign from argument values) - :returns args back - """ - for key, value in args.items(): - if isinstance(value, str) and value.startswith('='): - args[key] = value[1:] - return args diff --git a/SoftLayer/CLI/image/__init__.py b/SoftLayer/CLI/image/__init__.py new file mode 100644 index 000000000..a186c27d3 --- /dev/null +++ b/SoftLayer/CLI/image/__init__.py @@ -0,0 +1,11 @@ +"""Compute images.""" +# :license: MIT, see LICENSE for more details. +from SoftLayer.CLI import formatting + + +MASK = ('id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,' + 'imageType') +DETAIL_MASK = MASK + (',children[id,blockDevicesDiskSpaceTotal,datacenter],' + 'note,createDate,status') +PUBLIC_TYPE = formatting.FormattedItem('PUBLIC', 'Public') +PRIVATE_TYPE = formatting.FormattedItem('PRIVATE', 'Private') diff --git a/SoftLayer/CLI/image/delete.py b/SoftLayer/CLI/image/delete.py new file mode 100644 index 000000000..6cfc903b7 --- /dev/null +++ b/SoftLayer/CLI/image/delete.py @@ -0,0 +1,20 @@ +"""Delete an image.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Delete an image.""" + + image_mgr = SoftLayer.ImageManager(env.client) + image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') + + image_mgr.delete_image(image_id) diff --git a/SoftLayer/CLI/image/detail.py b/SoftLayer/CLI/image/detail.py new file mode 100644 index 000000000..c716c2299 --- /dev/null +++ b/SoftLayer/CLI/image/detail.py @@ -0,0 +1,59 @@ +"""Get details for an image.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer.CLI import image as image_mod +from SoftLayer import utils + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Get details for an image.""" + + image_mgr = SoftLayer.ImageManager(env.client) + image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') + + image = image_mgr.get_image(image_id, mask=image_mod.DETAIL_MASK) + disk_space = 0 + datacenters = [] + for child in image.get('children'): + disk_space = int(child.get('blockDevicesDiskSpaceTotal', 0)) + if child.get('datacenter'): + datacenters.append(utils.lookup(child, 'datacenter', 'name')) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + table.add_row(['id', image['id']]) + table.add_row(['global_identifier', + image.get('globalIdentifier', formatting.blank())]) + table.add_row(['name', image['name'].strip()]) + table.add_row(['status', formatting.FormattedItem( + utils.lookup(image, 'status', 'keyname'), + utils.lookup(image, 'status', 'name'), + )]) + table.add_row(['account', image.get('accountId', formatting.blank())]) + table.add_row(['visibility', + image_mod.PUBLIC_TYPE if image['publicFlag'] + else image_mod.PRIVATE_TYPE]) + table.add_row(['type', + formatting.FormattedItem( + utils.lookup(image, 'imageType', 'keyName'), + utils.lookup(image, 'imageType', 'name'), + )]) + table.add_row(['flex', image.get('flexImageFlag')]) + table.add_row(['note', image.get('note')]) + table.add_row(['created', image.get('createDate')]) + table.add_row(['disk_space', formatting.b_to_gb(disk_space)]) + table.add_row(['datacenters', formatting.listing(sorted(datacenters), + separator=',')]) + + return table diff --git a/SoftLayer/CLI/image/edit.py b/SoftLayer/CLI/image/edit.py new file mode 100644 index 000000000..4f4d68892 --- /dev/null +++ b/SoftLayer/CLI/image/edit.py @@ -0,0 +1,31 @@ +"""Edit details of an image.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--name', help="Name of the image") +@click.option('--note', help="Additional note for the image") +@click.option('--tag', help="Tags for the image") +@environment.pass_env +def cli(env, identifier, name, note, tag): + """Edit details of an image.""" + + image_mgr = SoftLayer.ImageManager(env.client) + data = {} + if name: + data['name'] = name + if note: + data['note'] = note + if tag: + data['tag'] = tag + image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') + if not image_mgr.edit(image_id, **data): + raise exceptions.CLIAbort("Failed to Edit Image") diff --git a/SoftLayer/CLI/image/list.py b/SoftLayer/CLI/image/list.py new file mode 100644 index 000000000..91901e97c --- /dev/null +++ b/SoftLayer/CLI/image/list.py @@ -0,0 +1,55 @@ +"""List images.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import image as image_mod +from SoftLayer import utils + +import click + + +@click.command() +@click.option('--public/--private', + is_flag=True, + default=None, + help='Display only public or private images') +@environment.pass_env +def cli(env, public): + """List images.""" + + image_mgr = SoftLayer.ImageManager(env.client) + + images = [] + if public in [False, None]: + for image in image_mgr.list_private_images(mask=image_mod.MASK): + images.append(image) + + if public in [True, None]: + for image in image_mgr.list_public_images(mask=image_mod.MASK): + images.append(image) + + table = formatting.Table(['id', + 'account', + 'name', + 'type', + 'visibility', + 'global_identifier']) + + images = [image for image in images if image['parentId'] == ''] + for image in images: + + table.add_row([ + image['id'], + image.get('accountId', formatting.blank()), + image['name'].strip(), + formatting.FormattedItem( + utils.lookup(image, 'imageType', 'keyName'), + utils.lookup(image, 'imageType', 'name')), + image_mod.PUBLIC_TYPE if image['publicFlag'] + else image_mod.PRIVATE_TYPE, + image.get('globalIdentifier', formatting.blank()), + ]) + + return table diff --git a/SoftLayer/CLI/iscsi/__init__.py b/SoftLayer/CLI/iscsi/__init__.py new file mode 100644 index 000000000..2b379049a --- /dev/null +++ b/SoftLayer/CLI/iscsi/__init__.py @@ -0,0 +1 @@ +"""iSCSI storage.""" diff --git a/SoftLayer/CLI/iscsi/cancel.py b/SoftLayer/CLI/iscsi/cancel.py new file mode 100644 index 000000000..ea0e1cd7d --- /dev/null +++ b/SoftLayer/CLI/iscsi/cancel.py @@ -0,0 +1,30 @@ +"""Cancel an existing iSCSI account.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--reason', help="An optional reason for cancellation") +@click.option('--immediate', + is_flag=True, + help="Cancels the iSCSI immediately instead of on the billing " + "anniversary") +@environment.pass_env +def cli(env, identifier, reason, immediate): + """Cancel an existing iSCSI account.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, identifier, 'iSCSI') + + if env.skip_confirmations or formatting.no_going_back(iscsi_id): + iscsi_mgr.cancel_iscsi(iscsi_id, reason, immediate) + else: + raise exceptions.CLIAbort('Aborted') diff --git a/SoftLayer/CLI/iscsi/create.py b/SoftLayer/CLI/iscsi/create.py new file mode 100644 index 000000000..0a8a480d4 --- /dev/null +++ b/SoftLayer/CLI/iscsi/create.py @@ -0,0 +1,23 @@ +"""Creates an iSCSI target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.option('--size', + type=click.INT, + required=True, + help="Size of the iSCSI volume to create (in gibibytes)") +@click.option('--datacenter', + required=True, + help="Datacenter shortname (sng01, dal05, ...)") +@environment.pass_env +def cli(env, size, datacenter): + """Creates an iSCSI target.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + iscsi_mgr.create_iscsi(size=size, location=datacenter) diff --git a/SoftLayer/CLI/iscsi/detail.py b/SoftLayer/CLI/iscsi/detail.py new file mode 100644 index 000000000..b920b41c5 --- /dev/null +++ b/SoftLayer/CLI/iscsi/detail.py @@ -0,0 +1,54 @@ +"""Get details for an iSCSI target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer import utils + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--password', + is_flag=True, + help="Show credentials to access the iSCSI target") +@environment.pass_env +def cli(env, identifier, password): + """Get details for an iSCSI target.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + + iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, identifier, 'iSCSI') + result = iscsi_mgr.get_iscsi(iscsi_id) + result = utils.NestedDict(result) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + table.add_row(['id', result['id']]) + table.add_row(['serviceResourceName', result['serviceResourceName']]) + table.add_row(['createDate', result['createDate']]) + table.add_row(['nasType', result['nasType']]) + table.add_row(['capacityGb', result['capacityGb']]) + + if result['snapshotCapacityGb']: + table.add_row(['snapshotCapacityGb', result['snapshotCapacityGb']]) + + table.add_row(['mountableFlag', result['mountableFlag']]) + table.add_row(['serviceResourceBackendIpAddress', + result['serviceResourceBackendIpAddress']]) + table.add_row(['price', result['billingItem']['recurringFee']]) + table.add_row(['BillingItemId', result['billingItem']['id']]) + if result.get('notes'): + table.add_row(['notes', result['notes']]) + + if password: + pass_table = formatting.Table(['username', 'password']) + pass_table.add_row([result['username'], result['password']]) + table.add_row(['users', pass_table]) + + return table diff --git a/SoftLayer/CLI/iscsi/list.py b/SoftLayer/CLI/iscsi/list.py new file mode 100644 index 000000000..60923ce3f --- /dev/null +++ b/SoftLayer/CLI/iscsi/list.py @@ -0,0 +1,40 @@ +"""List iSCSI targets.""" +# :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() +@environment.pass_env +def cli(env): + """List iSCSI targets.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + iscsi_list = iscsi_mgr.list_iscsi() + iscsi_list = [utils.NestedDict(n) for n in iscsi_list] + table = formatting.Table([ + 'id', + 'datacenter', + 'size', + 'username', + 'password', + 'server' + ]) + for iscsi in iscsi_list: + table.add_row([ + iscsi['id'], + iscsi['serviceResource']['datacenter'].get('name', + formatting.blank()), + formatting.FormattedItem(iscsi.get('capacityGb', + formatting.blank()), + "%dGB" % iscsi.get('capacityGb', 0)), + iscsi.get('username', formatting.blank()), + iscsi.get('password', formatting.blank()), + iscsi.get('serviceResourceBackendIpAddress', + formatting.blank())]) + return table diff --git a/SoftLayer/CLI/loadbal/__init__.py b/SoftLayer/CLI/loadbal/__init__.py new file mode 100644 index 000000000..8f7becb62 --- /dev/null +++ b/SoftLayer/CLI/loadbal/__init__.py @@ -0,0 +1,12 @@ +"""Load balancers.""" + +from SoftLayer.CLI import exceptions + + +def parse_id(input_id): + """Parse the load balancer kind and actual id from the "kind:id" form.""" + parts = input_id.split(':') + if len(parts) != 2: + raise exceptions.CLIAbort( + 'Invalid ID %s: ID should be of the form "kind:id"' % input_id) + return parts[0], int(parts[1]) diff --git a/SoftLayer/CLI/loadbal/cancel.py b/SoftLayer/CLI/loadbal/cancel.py new file mode 100644 index 000000000..6ba2fa0d7 --- /dev/null +++ b/SoftLayer/CLI/loadbal/cancel.py @@ -0,0 +1,29 @@ +"""Cancel an existing load balancer.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Cancel an existing load balancer.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, loadbal_id = loadbal.parse_id(identifier) + + if any([env.skip_confirmations, + formatting.confirm("This action will cancel a load balancer. " + "Continue?")]): + mgr.cancel_lb(loadbal_id) + return 'Load Balancer with id %s is being cancelled!' % identifier + else: + raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/loadbal/create.py b/SoftLayer/CLI/loadbal/create.py new file mode 100644 index 000000000..e253d62c7 --- /dev/null +++ b/SoftLayer/CLI/loadbal/create.py @@ -0,0 +1,25 @@ +"""Adds a load balancer given the id returned from create-options.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('billing-id') +@click.option('--datacenter', '-d', + help='Datacenter shortname (sng01, dal05, ...)') +@environment.pass_env +def cli(env, billing_id, datacenter): + """Adds a load balancer given the id returned from create-options.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + if not formatting.confirm("This action will incur charges on your " + "account. Continue?"): + raise exceptions.CLIAbort('Aborted.') + mgr.add_local_lb(billing_id, datacenter=datacenter) + return "Load balancer is being created!" diff --git a/SoftLayer/CLI/loadbal/create_options.py b/SoftLayer/CLI/loadbal/create_options.py new file mode 100644 index 000000000..386256fb9 --- /dev/null +++ b/SoftLayer/CLI/loadbal/create_options.py @@ -0,0 +1,35 @@ +"""Show load balancer options.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """Reset connections on a certain service group.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + table = formatting.Table(['price_id', 'capacity', 'description', 'price']) + + table.sortby = 'price' + table.align['price'] = 'r' + table.align['capacity'] = 'r' + table.align['id'] = 'r' + + packages = mgr.get_lb_pkgs() + + for package in packages: + table.add_row([ + package['prices'][0]['id'], + package.get('capacity'), + package['description'], + '%.2f' % float(package['prices'][0]['recurringFee']) + ]) + + return table diff --git a/SoftLayer/CLI/loadbal/detail.py b/SoftLayer/CLI/loadbal/detail.py new file mode 100644 index 000000000..9e3a817ec --- /dev/null +++ b/SoftLayer/CLI/loadbal/detail.py @@ -0,0 +1,85 @@ +"""Get Load balancer details.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Get Load balancer details.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, loadbal_id = loadbal.parse_id(identifier) + + load_balancer = mgr.get_local_lb(loadbal_id) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'l' + table.align['Value'] = 'l' + table.add_row(['General properties', '----------']) + table.add_row([' ID', 'local:%s' % load_balancer['id']]) + table.add_row([' IP Address', load_balancer['ipAddress']['ipAddress']]) + name = load_balancer['loadBalancerHardware'][0]['datacenter']['name'] + table.add_row([' Datacenter', name]) + table.add_row([' Connections limit', load_balancer['connectionLimit']]) + table.add_row([' Dedicated', load_balancer['dedicatedFlag']]) + table.add_row([' HA', load_balancer['highAvailabilityFlag']]) + table.add_row([' SSL Enabled', load_balancer['sslEnabledFlag']]) + table.add_row([' SSL Active', load_balancer['sslActiveFlag']]) + index0 = 1 + for virtual_server in load_balancer['virtualServers']: + table.add_row(['Service group %s' % index0, + '**************']) + index0 += 1 + table2 = formatting.Table(['Service group ID', + 'Port', + 'Allocation', + 'Routing type', + 'Routing Method']) + + for group in virtual_server['serviceGroups']: + table2.add_row([ + '%s:%s' % (load_balancer['id'], virtual_server['id']), + virtual_server['port'], + '%s %%' % virtual_server['allocation'], + '%s:%s' % (group['routingTypeId'], + group['routingType']['name']), + '%s:%s' % (group['routingMethodId'], + group['routingMethod']['name']) + ]) + + table.add_row([' Group Properties', table2]) + + table3 = formatting.Table(['Service_ID', + 'IP Address', + 'Port', + 'Health Check', + 'Weight', + 'Enabled', + 'Status']) + service_exist = False + for service in group['services']: + service_exist = True + health_check = service['healthChecks'][0] + table3.add_row([ + '%s:%s' % (load_balancer['id'], service['id']), + service['ipAddress']['ipAddress'], + service['port'], + '%s:%s' % (health_check['healthCheckTypeId'], + health_check['type']['name']), + service['groupReferences'][0]['weight'], + service['enabled'], + service['status'] + ]) + if service_exist: + table.add_row([' Services', table3]) + else: + table.add_row([' Services', 'None']) + return table diff --git a/SoftLayer/CLI/loadbal/group_add.py b/SoftLayer/CLI/loadbal/group_add.py new file mode 100644 index 000000000..76456f2b3 --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_add.py @@ -0,0 +1,41 @@ +"""Adds a new load_balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--allocation', + required=True, + type=click.INT, + help="The allocated percent of connections") +@click.option('--port', + required=True, + help="The port number", + type=click.INT) +@click.option('--routing-type', + required=True, + help="The port routing type") +@click.option('--routing-method', + required=True, + help="The routing method") +@environment.pass_env +def cli(env, identifier, allocation, port, routing_type, routing_method): + """Adds a new load_balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, loadbal_id = loadbal.parse_id(identifier) + + mgr.add_service_group(loadbal_id, + allocation=allocation, + port=port, + routing_type=routing_type, + routing_method=routing_method) + + return 'Load balancer service group is being added!' diff --git a/SoftLayer/CLI/loadbal/group_delete.py b/SoftLayer/CLI/loadbal/group_delete.py new file mode 100644 index 000000000..075dbe899 --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_delete.py @@ -0,0 +1,28 @@ +"""Deletes an existing load balancer service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Deletes an existing load balancer service group.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, group_id = loadbal.parse_id(identifier) + + if env.skip_confirmations or formatting.confirm("This action will cancel " + "a service group. " + "Continue?"): + mgr.delete_service_group(group_id) + return 'Service group %s is being deleted!' % identifier + else: + raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/loadbal/group_edit.py b/SoftLayer/CLI/loadbal/group_edit.py new file mode 100644 index 000000000..6de786284 --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_edit.py @@ -0,0 +1,41 @@ +"""Edit an existing load balancer service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--allocation', + type=click.INT, + help="Change the allocated percent of connections") +@click.option('--port', + help="Change the port number", + type=click.INT) +@click.option('--routing-type', + help="Change the port routing type") +@click.option('--routing-method', + help="Change the routing method") +@environment.pass_env +def cli(env, identifier, allocation, port, routing_type, routing_method): + """Edit an existing load balancer service group.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, group_id = loadbal.parse_id(identifier) + + # check if any input is provided + if not any([allocation, port, routing_type, routing_method]): + return 'At least one property is required to be changed!' + + mgr.edit_service_group(loadbal_id, + group_id, + allocation=allocation, + port=port, + routing_type=routing_type, + routing_method=routing_method) + + return 'Load balancer service group %s is being updated!' % identifier diff --git a/SoftLayer/CLI/loadbal/group_reset.py b/SoftLayer/CLI/loadbal/group_reset.py new file mode 100644 index 000000000..78b227d4e --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_reset.py @@ -0,0 +1,21 @@ +"""Reset connections on a certain service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Reset connections on a certain service group.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, group_id = loadbal.parse_id(identifier) + + mgr.reset_service_group(loadbal_id, group_id) + return 'Load balancer service group connections are being reset!' diff --git a/SoftLayer/CLI/loadbal/health_checks.py b/SoftLayer/CLI/loadbal/health_checks.py new file mode 100644 index 000000000..7f73e0b3a --- /dev/null +++ b/SoftLayer/CLI/loadbal/health_checks.py @@ -0,0 +1,26 @@ +"""List health check types.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List health check types.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + hc_types = mgr.get_hc_types() + table = formatting.KeyValueTable(['ID', 'Name']) + table.align['ID'] = 'l' + table.align['Name'] = 'l' + table.sortby = 'ID' + for hc_type in hc_types: + table.add_row([hc_type['id'], hc_type['name']]) + + return table diff --git a/SoftLayer/CLI/loadbal/list.py b/SoftLayer/CLI/loadbal/list.py new file mode 100644 index 000000000..a462ef9b1 --- /dev/null +++ b/SoftLayer/CLI/loadbal/list.py @@ -0,0 +1,49 @@ +"""List active load balancers.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List active load balancers.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + load_balancers = mgr.get_local_lbs() + + table = formatting.Table(['ID', + 'VIP Address', + 'Location', + 'SSL Offload', + 'Connections/second', + 'Type']) + + table.align['Connections/second'] = 'r' + + for load_balancer in load_balancers: + ssl_support = 'Not Supported' + if load_balancer['sslEnabledFlag']: + if load_balancer['sslActiveFlag']: + ssl_support = 'On' + else: + ssl_support = 'Off' + lb_type = 'Standard' + if load_balancer['dedicatedFlag']: + lb_type = 'Dedicated' + elif load_balancer['highAvailabilityFlag']: + lb_type = 'HA' + table.add_row([ + 'local:%s' % load_balancer['id'], + load_balancer['ipAddress']['ipAddress'], + load_balancer['loadBalancerHardware'][0]['datacenter']['name'], + ssl_support, + load_balancer['connectionLimit'], + lb_type + ]) + + return table diff --git a/SoftLayer/CLI/loadbal/routing_methods.py b/SoftLayer/CLI/loadbal/routing_methods.py new file mode 100644 index 000000000..4549e5ece --- /dev/null +++ b/SoftLayer/CLI/loadbal/routing_methods.py @@ -0,0 +1,25 @@ +"""List routing methods.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List routing types.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + routing_methods = mgr.get_routing_methods() + table = formatting.KeyValueTable(['ID', 'Name']) + table.align['ID'] = 'l' + table.align['Name'] = 'l' + table.sortby = 'ID' + for routing_method in routing_methods: + table.add_row([routing_method['id'], routing_method['name']]) + + return table diff --git a/SoftLayer/CLI/loadbal/routing_types.py b/SoftLayer/CLI/loadbal/routing_types.py new file mode 100644 index 000000000..b3b76060c --- /dev/null +++ b/SoftLayer/CLI/loadbal/routing_types.py @@ -0,0 +1,24 @@ +"""List routing types.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List routing types.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + routing_methods = mgr.get_routing_methods() + table = formatting.KeyValueTable(['ID', 'Name']) + table.align['ID'] = 'l' + table.align['Name'] = 'l' + table.sortby = 'ID' + for routing_method in routing_methods: + table.add_row([routing_method['id'], routing_method['name']]) + return table diff --git a/SoftLayer/CLI/loadbal/service_add.py b/SoftLayer/CLI/loadbal/service_add.py new file mode 100644 index 000000000..5a9fa982a --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_add.py @@ -0,0 +1,52 @@ +"""Adds a new load balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--enabled / --disabled', + required=True, + help="Create the service as enable or disabled") +@click.option('--port', + required=True, + help="The port number for the service", + type=click.INT) +@click.option('--weight', + required=True, + type=click.INT, + help="The weight of the service") +@click.option('--healthcheck-type', + required=True, + help="The health check type") +@click.option('--ip-address', '--ip', + required=True, + help="The IP of the service") +@environment.pass_env +def cli(env, identifier, enabled, port, weight, healthcheck_type, ip_address): + """Adds a new load balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, group_id = loadbal.parse_id(identifier) + + # check if the IP is valid + ip_address_id = None + if ip_address: + ip_service = env.client['Network_Subnet_IpAddress'] + ip_record = ip_service.getByIpAddress(ip_address) + ip_address_id = ip_record['id'] + + mgr.add_service(loadbal_id, + group_id, + ip_address_id=ip_address_id, + enabled=enabled, + port=port, + weight=weight, + hc_type=healthcheck_type) + return 'Load balancer service is being added!' diff --git a/SoftLayer/CLI/loadbal/service_delete.py b/SoftLayer/CLI/loadbal/service_delete.py new file mode 100644 index 000000000..8b4d43f7b --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_delete.py @@ -0,0 +1,28 @@ +"""Deletes an existing load balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Deletes an existing load balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + _, service_id = loadbal.parse_id(identifier) + + if env.skip_confirmations or formatting.confirm("This action will cancel " + "a service from your load " + "balancer. Continue?"): + mgr.delete_service(service_id) + return 'Load balancer service %s is being cancelled!' % service_id + else: + raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/loadbal/service_edit.py b/SoftLayer/CLI/loadbal/service_edit.py new file mode 100644 index 000000000..0ae024edc --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_edit.py @@ -0,0 +1,49 @@ +"""Edit the properties of a service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--enabled / --disabled', + default=None, + help="Enable or disable the service") +@click.option('--port', + help="Change the port number for the service", type=click.INT) +@click.option('--weight', + type=click.INT, + help="Change the weight of the service") +@click.option('--healthcheck-type', help="Change the health check type") +@click.option('--ip-address', '--ip', help="Change the IP of the service") +@environment.pass_env +def cli(env, identifier, enabled, port, weight, healthcheck_type, ip_address): + """Edit the properties of a service group.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, service_id = loadbal.parse_id(identifier) + + # check if any input is provided + if not any([ip_address, enabled, weight, port, healthcheck_type]): + return 'At least one property is required to be changed!' + + # check if the IP is valid + ip_address_id = None + if ip_address: + ip_service = env.client['Network_Subnet_IpAddress'] + ip_record = ip_service.getByIpAddress(ip_address) + ip_address_id = ip_record['id'] + + mgr.edit_service(loadbal_id, + service_id, + ip_address_id=ip_address_id, + enabled=enabled, + port=port, + weight=weight, + hc_type=healthcheck_type) + return 'Load balancer service %s is being modified!' % identifier diff --git a/SoftLayer/CLI/loadbal/service_toggle.py b/SoftLayer/CLI/loadbal/service_toggle.py new file mode 100644 index 000000000..7f4090e75 --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_toggle.py @@ -0,0 +1,28 @@ +"""Toggle the status of an existing load balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Toggle the status of an existing load balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + _, service_id = loadbal.parse_id(identifier) + + if env.skip_confirmations or formatting.confirm("This action will toggle " + "the status on the " + "service. Continue?"): + mgr.toggle_service_status(service_id) + return 'Load balancer service %s status updated!' % identifier + else: + raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/metadata.py b/SoftLayer/CLI/metadata.py new file mode 100644 index 000000000..150ea3c6e --- /dev/null +++ b/SoftLayer/CLI/metadata.py @@ -0,0 +1,67 @@ +"""Find details about this machine.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + +META_MAPPING = { + 'backend_ip': 'primary_backend_ip', + 'ip': 'primary_ip', +} + + +@click.command(epilog="These commands only work on devices on the backend " + "SoftLayer network. This allows for self-discovery for " + "newly provisioned resources.") +@click.argument('prop', type=click.Choice(['backend_ip', + 'backend_mac', + 'datacenter', + 'datacenter_id', + 'fqdn', + 'frontend_mac', + 'id', + 'ip', + 'network', + 'provision_state', + 'tags', + 'user_data'])) +def cli(prop): + """Find details about this machine.""" + + try: + if prop == 'network': + return get_network() + + meta_prop = META_MAPPING.get(prop) or prop + return SoftLayer.MetadataManager().get(meta_prop) + except SoftLayer.TransportError: + raise exceptions.CLIAbort( + 'Cannot connect to the backend service address. Make sure ' + 'this command is being ran from a device on the backend ' + 'network.') + + +def get_network(): + """Returns a list of tables with public and private network details.""" + meta = SoftLayer.MetadataManager() + network_tables = [] + for network_func in [meta.public_network, meta.private_network]: + network = network_func() + + table = formatting.KeyValueTable(['name', 'value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + table.add_row(['mac addresses', + formatting.listing(network['mac_addresses'], + separator=',')]) + table.add_row(['router', network['router']]) + table.add_row(['vlans', + formatting.listing(network['vlans'], separator=',')]) + table.add_row(['vlan ids', + formatting.listing(network['vlan_ids'], separator=',')]) + network_tables.append(table) + + return network_tables diff --git a/SoftLayer/CLI/modules/__init__.py b/SoftLayer/CLI/modules/__init__.py deleted file mode 100644 index eaad02f96..000000000 --- a/SoftLayer/CLI/modules/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" - SoftLayer.CLI.modules - ~~~~~~~~~~~~~~~~~~~~~ - Contains all plugable modules for the CLI interface - - :license: MIT, see LICENSE for more details. -""" - -import pkgutil - - -def get_module_list(): - """Returns each module under SoftLayer.CLI.modules.""" - actions = [action[1] for action in pkgutil.iter_modules(__path__)] - return actions diff --git a/SoftLayer/CLI/modules/cdn.py b/SoftLayer/CLI/modules/cdn.py deleted file mode 100644 index 11f0e8b47..000000000 --- a/SoftLayer/CLI/modules/cdn.py +++ /dev/null @@ -1,188 +0,0 @@ -""" -usage: sl cdn [] [...] [options] - -Manage CDN accounts and configuration - -The available commands are: - detail Show details for a CDN account - list List CDN accounts - load Cache one or more files on all edge nodes - origin-add Add an origin pull mapping - origin-list Show origin pull mappings on a CDN account - origin-remove Remove an origin pull mapping - purge Purge one or more cached files from all edge nodes -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -class ListAccounts(environment.CLIRunnable): - """ -usage: sl cdn list [options] - -List all CDN accounts - -Options: - --sortby=SORTBY Sort by this value. [Default: id] - [Options: id, account_name, type, created, notes] -""" - action = 'list' - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - accounts = manager.list_accounts() - - table = formatting.Table(['id', - 'account_name', - 'type', - 'created', - 'notes']) - for account in accounts: - table.add_row([ - account['id'], - account['cdnAccountName'], - account['cdnSolutionName'], - account['createDate'], - account.get('cdnAccountNote', formatting.blank()) - ]) - - table.sortby = args['--sortby'] - return table - - -class DetailAccount(environment.CLIRunnable): - """ -usage: sl cdn detail [options] - -Show CDN account details -""" - action = 'detail' - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - account = manager.get_account(args.get('')) - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - table.add_row(['id', account['id']]) - table.add_row(['account_name', account['cdnAccountName']]) - table.add_row(['type', account['cdnSolutionName']]) - table.add_row(['status', account['status']['name']]) - table.add_row(['created', account['createDate']]) - table.add_row(['notes', - account.get('cdnAccountNote', formatting.blank())]) - - return table - - -class LoadContent(environment.CLIRunnable): - """ -usage: sl cdn load ... [options] - -Cache one or more files on all edge nodes - -Required: - account The CDN account ID to cache content in - content_url The CDN URL(s) or CDN CNAME-based URL(s) for the content - you wish to cache (can be repeated) -""" - action = 'load' - required_params = ['account', 'content_url'] - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - manager.load_content(args.get(''), args.get('')) - - -class PurgeContent(environment.CLIRunnable): - """ -usage: sl cdn purge ... [options] - -Purge one or more cached files from all edge nodes - -Required: - account The CDN account ID to purge content from - content_url The CDN URL(s) or CDN CNAME-based URL(s) for the content - you wish to cache (can be repeated) -""" - action = 'purge' - required_params = ['account', 'content_url'] - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - manager.purge_content(args.get(''), - args.get('')) - - -class ListOrigins(environment.CLIRunnable): - """ -usage: sl cdn origin-list [options] - -List origin pull mappings associated with a CDN account. -""" - action = 'origin-list' - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - origins = manager.get_origins(args.get('')) - - table = formatting.Table(['id', 'media_type', 'cname', 'origin_url']) - - for origin in origins: - table.add_row([origin['id'], - origin['mediaType'], - origin.get('cname', formatting.blank()), - origin['originUrl']]) - - return table - - -class AddOrigin(environment.CLIRunnable): - """ -usage: sl cdn origin-add [options] - -Create an origin pull mapping on a CDN account - -Required: - account The CDN account ID to create a mapping on - url A full URL where content should be pulled from by - CDN edge nodes - -Options: - --type=TYPE The media type for this mapping (http, flash, wm, ...) - (default: http) - --cname=CNAME An optional CNAME to attach to the mapping -""" - action = 'origin-add' - required_params = ['account', 'url'] - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - media_type = args.get('--type') or 'http' - - manager.add_origin(args.get(''), media_type, - args.get(''), args.get('--cname', None)) - - -class RemoveOrigin(environment.CLIRunnable): - """ -usage: sl cdn origin-remove [options] - -Remove an origin pull mapping from a CDN account - -Required: - account The CDN account ID to remove a mapping from - origin_id The origin mapping ID to remove -""" - action = 'origin-remove' - required_params = ['account', 'origin_id'] - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - manager.remove_origin(args.get(''), - args.get('')) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py deleted file mode 100644 index 3f84e1052..000000000 --- a/SoftLayer/CLI/modules/config.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -usage: sl config [] [...] [options] - -View and edit configuration - -The available commands are: - setup Setup configuration - show Show current configuration -""" -# :license: MIT, see LICENSE for more details. - -import os.path - -import SoftLayer -from SoftLayer import auth -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer import utils - - -def get_settings_from_client(client): - """ Pull out settings from a SoftLayer.Client instance. - - :param client: SoftLayer.Client instance - """ - settings = { - 'username': '', - 'api_key': '', - 'timeout': client.timeout or '', - 'endpoint_url': client.endpoint_url, - } - try: - settings['username'] = client.auth.username - settings['api_key'] = client.auth.api_key - except AttributeError: - pass - - return settings - - -def config_table(settings): - """ Returns a config table """ - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - table.add_row(['Username', settings['username'] or 'not set']) - table.add_row(['API Key', settings['api_key'] or 'not set']) - table.add_row(['Endpoint URL', settings['endpoint_url'] or 'not set']) - table.add_row(['Timeout', settings['timeout'] or 'not set']) - return table - - -def get_api_key(client, username, secret, endpoint_url=None): - """ Attempts API-Key and password auth to get an API key - - This will also generate an API key if one doesn't exist - """ - - client.endpoint_url = endpoint_url - client.auth = None - # Try to use a client with username/api key - if len(secret) == 64: - try: - client.auth = auth.BasicAuthentication(username, secret) - client['Account'].getCurrentUser() - return secret - except SoftLayer.SoftLayerAPIError as ex: - if 'invalid api token' not in ex.faultString.lower(): - raise - else: - # Try to use a client with username/password - client.authenticate_with_password(username, secret) - - user_record = client['Account'].getCurrentUser( - mask='id, apiAuthenticationKeys') - api_keys = user_record['apiAuthenticationKeys'] - if len(api_keys) == 0: - return client['User_Customer'].addApiAuthenticationKey( - id=user_record['id']) - return api_keys[0]['authenticationKey'] - - -class Setup(environment.CLIRunnable): - """ -usage: sl config setup [options] - -Setup configuration -""" - action = 'setup' - - def execute(self, args): - username, secret, endpoint_url, timeout = self.get_user_input() - - api_key = get_api_key(self.client, username, secret, - endpoint_url=endpoint_url) - - path = '~/.softlayer' - if args.get('--config'): - path = args.get('--config') - config_path = os.path.expanduser(path) - - self.env.out( - formatting.format_output(config_table({ - 'username': username, - 'api_key': api_key, - 'endpoint_url': endpoint_url, - 'timeout': timeout}))) - - if not formatting.confirm('Are you sure you want to write settings ' - 'to "%s"?' % config_path, default=True): - raise exceptions.CLIAbort('Aborted.') - - # Persist the config file. Read the target config file in before - # setting the values to avoid clobbering settings - config = utils.configparser.RawConfigParser() - config.read(config_path) - try: - config.add_section('softlayer') - except utils.configparser.DuplicateSectionError: - pass - - config.set('softlayer', 'username', username) - config.set('softlayer', 'api_key', api_key) - config.set('softlayer', 'endpoint_url', endpoint_url) - - config_file = os.fdopen(os.open(config_path, - (os.O_WRONLY | os.O_CREAT), - 0o600), - 'w') - try: - config.write(config_file) - finally: - config_file.close() - - return "Configuration Updated Successfully" - - def get_user_input(self): - """ Ask for username, secret (api_key or password) and endpoint_url """ - - defaults = get_settings_from_client(self.client) - timeout = defaults['timeout'] - - # Ask for username - for _ in range(3): - username = (self.env.input('Username [%s]: ' - % defaults['username']) - or defaults['username']) - if username: - break - else: - raise exceptions.CLIAbort('Aborted after 3 attempts') - - # Ask for 'secret' which can be api_key or their password - for _ in range(3): - secret = (self.env.getpass('API Key or Password [%s]: ' - % defaults['api_key']) - or defaults['api_key']) - if secret: - break - else: - raise exceptions.CLIAbort('Aborted after 3 attempts') - - # Ask for which endpoint they want to use - for _ in range(3): - endpoint_type = self.env.input( - 'Endpoint (public|private|custom): ') - endpoint_type = endpoint_type.lower() - if not endpoint_type: - endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT - break - if endpoint_type == 'public': - endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT - break - elif endpoint_type == 'private': - endpoint_url = SoftLayer.API_PRIVATE_ENDPOINT - break - elif endpoint_type == 'custom': - endpoint_url = self.env.input( - 'Endpoint URL [%s]: ' % defaults['endpoint_url'] - ) or defaults['endpoint_url'] - break - else: - raise exceptions.CLIAbort('Aborted after 3 attempts') - - return username, secret, endpoint_url, timeout - - -class Show(environment.CLIRunnable): - """ -usage: sl config show [options] - -Show current configuration -""" - action = 'show' - - def execute(self, args): - settings = get_settings_from_client(self.client) - return config_table(settings) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py deleted file mode 100755 index 90a995b87..000000000 --- a/SoftLayer/CLI/modules/dns.py +++ /dev/null @@ -1,369 +0,0 @@ -""" -usage: sl dns [] [...] [options] - -Manage DNS - -The available zone commands are: - create Create zone - delete Delete zone - list List zones or a zone's records - print Print zone in BIND format - -The available record commands are: - add Add resource record - edit Update resource records (bulk/single) - remove Remove resource records -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - -import re - -RECORD_REGEX = re.compile(r"""^((?P([\w-]+(\.)?)*|\@)?\s+ - (?P\d+)?\s+ - (?P\w+)?)?\s+ - (?P\w+)\s+ - (?P.*)""", re.X) - - -class DumpZone(environment.CLIRunnable): - """ -usage: sl dns print [options] - -print zone in BIND format - -Arguments: - Zone name (softlayer.com) -""" - action = "print" - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - return manager.dump_zone(zone_id) - - -class CreateZone(environment.CLIRunnable): - """ -usage: sl dns create [options] - -Create a zone - -Arguments: - Zone name (softlayer.com) -""" - action = 'create' - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - manager.create_zone(args['']) - - -class DeleteZone(environment.CLIRunnable): - """ -usage: sl dns delete [options] - -Delete zone - -Arguments: - Zone name (softlayer.com) -""" - action = 'delete' - options = ['confirm'] - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - if args['--really'] or formatting.no_going_back(args['']): - manager.delete_zone(zone_id) - else: - raise exceptions.CLIAbort("Aborted.") - - -class ImportZone(environment.CLIRunnable): - """ -usage: sl dns import [options] - -Creates a new zone based off a nicely BIND formatted file - -Arguments: - Path to the bind zone file you want to import -Options: - --dry-run Don't actually do anything. This will show you what is parsed - - """ - action = 'import' - - def execute(self, args): - - dry_run = args.get('--dry-run') - - manager = SoftLayer.DNSManager(self.client) - with open(args['']) as zone_file: - zone_contents = zone_file.read() - - zone, records, bad_lines = parse_zone_details(zone_contents) - - self.env.out("Parsed: zone=%s" % zone) - for record in records: - self.env.out("Parsed: %s" % record) - for line in bad_lines: - self.env.out("Unparsed: %s" % line) - - if dry_run: - return - - # Find zone id or create the zone if it doesn't exist - try: - zone_id = helpers.resolve_id(manager.resolve_ids, zone, - name='zone') - except exceptions.CLIAbort: - zone_id = manager.create_zone(zone)['id'] - self.env.out("\033[92mCREATED ZONE: %s\033[0m" % zone) - - # Attempt to create each record - for record in records: - try: - manager.create_record(zone_id, - record['record'], - record['record_type'], - record['data'], - record['ttl']) - self.env.out("\033[92mCreated: Host: %s\033[0m" % record) - except SoftLayer.SoftLayerAPIError as ex: - self.env.out("\033[91mFAILED: %s" % record) - self.env.out("%s \033[0m" % ex) - - return "Finished" - - -def parse_zone_details(zone_contents): - """Parses a zone file into python data-structures""" - records = [] - bad_lines = [] - zone_lines = [line.strip() for line in zone_contents.split('\n')] - - zone_search = re.search(r'^\$ORIGIN (?P.*)\.', zone_lines[0]) - zone = zone_search.group('zone') - - for line in zone_lines[1:]: - record_search = re.search(RECORD_REGEX, line) - if record_search is None: - bad_lines.append(line) - continue - - name = record_search.group('domain') - # The API requires we send a host, although bind allows a blank - # entry. @ is the same thing as blank - if name is None: - name = "@" - - ttl = record_search.group('ttl') - # we don't do anything with the class - # domain_class = domainSearch.group('class') - record_type = record_search.group('type').upper() - data = record_search.group('data') - - # the dns class doesn't support weighted MX records yet, so we chomp - # that part out. - if record_type == "MX": - record_search = re.search(r'(?P\d+)\s+(?P.*)', data) - data = record_search.group('data') - - # This will skip the SOA record bit. And any domain that gets - # parsed oddly. - if record_type == 'IN': - bad_lines.append(line) - continue - - records.append({ - 'record': name, - 'record_type': record_type, - 'data': data, - 'ttl': ttl, - }) - - return zone, records, bad_lines - - -class ListZones(environment.CLIRunnable): - """ -usage: sl dns list [] [options] - -List zones and optionally, records - -Filters: - --data=DATA Record data, such as an IP address - --record=HOST Host record, such as www - --ttl=TTL TTL value in seconds, such as 86400 - --type=TYPE Record type, such as A or CNAME -""" - action = 'list' - - def execute(self, args): - if args['']: - return self.list_zone(args) - - return self.list_all_zones() - - def list_zone(self, args): - """ list records for a particular zone """ - manager = SoftLayer.DNSManager(self.client) - table = formatting.Table(['id', 'record', 'type', 'ttl', 'value']) - - table.align['ttl'] = 'l' - table.align['record'] = 'r' - table.align['value'] = 'l' - - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - records = manager.get_records( - zone_id, - record_type=args.get('--type'), - host=args.get('--record'), - ttl=args.get('--ttl'), - data=args.get('--data'), - ) - - for record in records: - table.add_row([ - record['id'], - record['host'], - record['type'].upper(), - record['ttl'], - record['data'] - ]) - - return table - - def list_all_zones(self): - """ List all zones """ - manager = SoftLayer.DNSManager(self.client) - zones = manager.list_zones() - table = formatting.Table(['id', 'zone', 'serial', 'updated']) - table.align['serial'] = 'c' - table.align['updated'] = 'c' - - for zone in zones: - table.add_row([ - zone['id'], - zone['name'], - zone['serial'], - zone['updateDate'], - ]) - - return table - - -class AddRecord(environment.CLIRunnable): - """ -usage: sl dns add [--ttl=TTL] [options] - -Add resource record - -Arguments: - Zone name (softlayer.com) - Resource record (www) - Record type. [Options: A, AAAA, - CNAME, MX, NS, PTR, SPF, SRV, TXT] - Record data. NOTE: only minor validation is done - -Options: - --ttl=TTL Time to live -""" - action = 'add' - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - manager.create_record( - zone_id, - args[''], - args[''], - args[''], - ttl=args['--ttl'] or 7200) - - -class EditRecord(environment.CLIRunnable): - """ -usage: sl dns edit [--data=DATA] [--ttl=TTL] [--id=ID] - [options] - -Update resource records (bulk/single) - -Arguments: - Zone name (softlayer.com) - Resource record (www) - -Options: - --data=DATA - --id=ID Modify only the given ID - --ttl=TTL Time to live -""" - action = 'edit' - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - results = manager.get_records( - zone_id, - host=args['']) - - for result in results: - if args['--id'] and str(result['id']) != args['--id']: - continue - result['data'] = args['--data'] or result['data'] - result['ttl'] = args['--ttl'] or result['ttl'] - manager.edit_record(result) - - -class RecordRemove(environment.CLIRunnable): - """ -usage: sl dns remove [--id=ID] [options] - -Remove resource records - -Arguments: - Zone name (softlayer.com) - Resource record (www) - -Options: - --id=ID Remove only the given ID -""" - action = 'remove' - options = ['confirm'] - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - if args['--id']: - records = [{'id': args['--id']}] - else: - records = manager.get_records( - zone_id, - host=args['']) - - if args['--really'] or formatting.no_going_back('yes'): - table = formatting.Table(['record']) - for result in records: - manager.delete_record(result['id']) - table.add_row([result['id']]) - - return table - raise exceptions.CLIAbort("Aborted.") diff --git a/SoftLayer/CLI/modules/filters.py b/SoftLayer/CLI/modules/filters.py deleted file mode 100644 index ae8073a6d..000000000 --- a/SoftLayer/CLI/modules/filters.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -usage: sl help filters - -Filters are used to limit the amount of results. Some commands will accept a -filter operation for certain fields. Filters can be applied across multiple -fields in most cases. - -Available Operations: - Case Insensitive - 'value' Exact value match - 'value*' Begins with value - '*value' Ends with value - '*value*' Contains value - - Case Sensitive - '~ value' Exact value match - '> value' Greater than value - '< value' Less than value - '>= value' Greater than or equal to value - '<= value' Less than or equal to value - -Examples: - sl server list --datacenter=dal05 - sl server list --hostname='prod*' - sl vs list --network=100 --cpu=2 - sl vs list --network='< 100' --cpu=2 - sl vs list --memory='>= 2048' - -Note: Comparison operators (>, <, >=, <=) can be used with integers, floats, - and strings. -""" -# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py deleted file mode 100755 index 489cf829a..000000000 --- a/SoftLayer/CLI/modules/firewall.py +++ /dev/null @@ -1,424 +0,0 @@ -""" -usage: sl firewall [] [...] [options] - -Firewall rule and security management - -The available commands are: - add Add a new firewall - cancel Cancel an existing firewall - detail Provide details about a particular firewall - edit Edit the rules of a particular firewall - list List active firewalls - both dedicated and shared - -""" -# :license: MIT, see LICENSE for more details. - -from __future__ import print_function -import os -import subprocess -import tempfile - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - -DELIMITER = "=========================================\n" - - -def get_ids(input_id): - """ Helper package to retrieve the actual IDs - :param input_id: the ID provided by the user - :returns: A list of valid IDs - """ - key_value = input_id.split(':') - - if len(key_value) != 2: - raise exceptions.CLIAbort( - 'Invalid ID %s: ID should be of the form xxx:yyy' % input_id) - return key_value - - -def print_package_info(package): - """ Helper package to print the firewall price. - - :param dict package: A dictionary representing the firewall package - """ - print("******************") - print("Product: %s" % package[0]['description']) - print("Price: %s$ monthly" % package[0]['prices'][0]['recurringFee']) - print("******************") - return - - -def has_firewall_component(server): - """ Helper to determine whether or not a server has a firewall. - - :param dict server: A dictionary representing a server - :returns: True if the Server has a firewall. - """ - if server['status'] != 'no_edit': - return True - - return False - - -def get_rules_table(rules): - """ Helper to format the rules into a table - - :param list rules: A list containing the rules of the firewall - :returns: a formatted table of the firewall rules - """ - table = formatting.Table(['#', 'action', 'protocol', 'src_ip', 'src_mask', - 'dest', 'dest_mask']) - table.sortby = '#' - for rule in rules: - table.add_row([ - rule['orderValue'], - rule['action'], - rule['protocol'], - rule['sourceIpAddress'], - rule['sourceIpSubnetMask'], - '%s:%s-%s' % (rule['destinationIpAddress'], - rule['destinationPortRangeStart'], - rule['destinationPortRangeEnd']), - rule['destinationIpSubnetMask']]) - return table - - -def get_formatted_rule(rule=None): - """ Helper to format the rule into a user friendly format - for editing purposes - - :param dict rule: A dict containing one rule of the firewall - :returns: a formatted string that get be pushed into the editor - """ - rule = rule or {} - return ('action: %s\n' - 'protocol: %s\n' - 'source_ip_address: %s\n' - 'source_ip_subnet_mask: %s\n' - 'destination_ip_address: %s\n' - 'destination_ip_subnet_mask: %s\n' - 'destination_port_range_start: %s\n' - 'destination_port_range_end: %s\n' - 'version: %s\n' - % (rule.get('action', 'permit'), - rule.get('protocol', 'tcp'), - rule.get('sourceIpAddress', 'any'), - rule.get('sourceIpSubnetMask', '255.255.255.255'), - rule.get('destinationIpAddress', 'any'), - rule.get('destinationIpSubnetMask', '255.255.255.255'), - rule.get('destinationPortRangeStart', 1), - rule.get('destinationPortRangeEnd', 1), - rule.get('version', 4))) - - -def open_editor(rules=None, content=None): - """ Helper to open an editor for editing the firewall rules - This method takes two parameters, if content is provided, - that means that submitting the rules failed and we are allowing - the user to re-edit what they provided. - If content is not provided, the rules retrieved from the firewall - will be displayed to the user. - - :param list rules: A list containing the rules of the firewall - :param string content: the content that the user provided in the editor - :returns: a formatted string that get be pushed into the editor - """ - - # Let's get the default EDITOR of the environment, - # use nano if none is specified - editor = os.environ.get('EDITOR', 'nano') - - with tempfile.NamedTemporaryFile(suffix=".tmp") as tfile: - - if content: - # if content is provided, just display it as is - tfile.write(content) - tfile.flush() - subprocess.call([editor, tfile.name]) - tfile.seek(0) - data = tfile.read() - return data - - if not rules: - # if the firewall has no rules, provide a template - tfile.write(DELIMITER) - tfile.write(get_formatted_rule()) - else: - # if the firewall has rules, display those to the user - for rule in rules: - tfile.write(DELIMITER) - tfile.write(get_formatted_rule(rule)) - tfile.write(DELIMITER) - tfile.flush() - subprocess.call([editor, tfile.name]) - tfile.seek(0) - data = tfile.read() - return data - - return - - -def parse_rules(content=None): - """ Helper to parse the input from the user into a list of rules. - - :param string content: the content of the editor - :returns: a list of rules - """ - rules = content.split(DELIMITER) - parsed_rules = list() - order = 1 - for rule in rules: - if rule.strip() == '': - continue - parsed_rule = {} - lines = rule.split("\n") - parsed_rule['orderValue'] = order - order += 1 - for line in lines: - if line.strip() == '': - continue - key_value = line.strip().split(':') - key = key_value[0].strip() - value = key_value[1].strip() - if key == 'action': - parsed_rule['action'] = value - elif key == 'protocol': - parsed_rule['protocol'] = value - elif key == 'source_ip_address': - parsed_rule['sourceIpAddress'] = value - elif key == 'source_ip_subnet_mask': - parsed_rule['sourceIpSubnetMask'] = value - elif key == 'destination_ip_address': - parsed_rule['destinationIpAddress'] = value - elif key == 'destination_ip_subnet_mask': - parsed_rule['destinationIpSubnetMask'] = value - elif key == 'destination_port_range_start': - parsed_rule['destinationPortRangeStart'] = int(value) - elif key == 'destination_port_range_end': - parsed_rule['destinationPortRangeEnd'] = int(value) - elif key == 'version': - parsed_rule['version'] = int(value) - parsed_rules.append(parsed_rule) - return parsed_rules - - -class FWList(environment.CLIRunnable): - """ -usage: sl firewall list [options] - -List active firewalls -""" - action = 'list' - - def execute(self, args): - mgr = SoftLayer.FirewallManager(self.client) - table = formatting.Table(['firewall id', - 'type', - 'features', - 'server/vlan id']) - fwvlans = mgr.get_firewalls() - dedicated_firewalls = [firewall for firewall in fwvlans - if firewall['dedicatedFirewallFlag']] - - for vlan in dedicated_firewalls: - features = [] - if vlan['highAvailabilityFirewallFlag']: - features.append('HA') - - if features: - feature_list = formatting.listing(features, separator=',') - else: - feature_list = formatting.blank() - - table.add_row([ - 'vlan:%s' % vlan['networkVlanFirewall']['id'], - 'VLAN - dedicated', - feature_list, - vlan['id'] - ]) - - shared_vlan = [firewall for firewall in fwvlans - if not firewall['dedicatedFirewallFlag']] - for vlan in shared_vlan: - vs_firewalls = [guest - for guest in vlan['firewallGuestNetworkComponents'] - if has_firewall_component(guest)] - - for firewall in vs_firewalls: - table.add_row([ - 'cci:%s' % firewall['id'], - 'CCI - standard', - '-', - firewall['guestNetworkComponent']['guest']['id'] - ]) - - server_firewalls = [server - for server in vlan['firewallNetworkComponents'] - if has_firewall_component(server)] - - for firewall in server_firewalls: - table.add_row([ - 'server:%s' % firewall['id'], - 'Server - standard', - '-', - utils.lookup(firewall, - 'networkComponent', - 'downlinkComponent', - 'hardwareId') - ]) - - return table - - -class FWCancel(environment.CLIRunnable): - """ -usage: sl firewall cancel [options] - -Cancels a firewall - -Options: - --really Whether to skip the confirmation prompt - -""" - action = 'cancel' - options = ['really'] - - def execute(self, args): - mgr = SoftLayer.FirewallManager(self.client) - input_id = args.get('') - key_value = get_ids(input_id) - firewall_id = int(key_value[1]) - - if args['--really'] or formatting.confirm("This action will cancel a " - "firewall from your account." - " Continue?"): - if key_value[0] in ['cci', 'server']: - mgr.cancel_firewall(firewall_id, dedicated=False) - elif key_value[0] == 'vlan': - mgr.cancel_firewall(firewall_id, dedicated=True) - return 'Firewall with id %s is being cancelled!' % input_id - else: - raise exceptions.CLIAbort('Aborted.') - - -class FWAdd(environment.CLIRunnable): - """ -usage: sl firewall add (--cci | --vlan | --server) [options] - -Adds a firewall of type either standard (cci or server) or dedicated(vlan) -Options: - --cci creates a standard firewall for a CCI - --vlan creates a dedicated firewall for a VLAN - --server creates a standard firewall for a server - --ha whether HA will be on or off - only for dedicated - --really whether to skip the confirmation prompt -""" - action = 'add' - options = ['really', 'ha'] - - def execute(self, args): - mgr = SoftLayer.FirewallManager(self.client) - input_id = helpers.resolve_id( - mgr.resolve_ids, args.get(''), 'firewall') - ha_support = args.get('--ha', False) - if not args['--really']: - if args['--vlan']: - pkg = mgr.get_dedicated_package(ha_enabled=ha_support) - elif args['--cci']: - pkg = mgr.get_standard_package(input_id) - elif args['--server']: - pkg = mgr.get_standard_package(input_id, is_cci=False) - - if not pkg: - return "Unable to add firewall - Is network public enabled?" - print_package_info(pkg) - - if not formatting.confirm("This action will incur charges on your " - "account. Continue?"): - raise exceptions.CLIAbort('Aborted.') - - if args['--vlan']: - mgr.add_vlan_firewall(input_id, ha_enabled=ha_support) - elif args['--cci']: - mgr.add_standard_firewall(input_id, is_cci=True) - elif args['--server']: - mgr.add_standard_firewall(input_id, is_cci=False) - - return "Firewall is being created!" - - -class FWDetails(environment.CLIRunnable): - """ -usage: sl firewall detail [options] - -Get firewall details -""" - action = 'detail' - - def execute(self, args): - mgr = SoftLayer.FirewallManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - if key_value[0] == 'vlan': - rules = mgr.get_dedicated_fwl_rules(key_value[1]) - else: - rules = mgr.get_standard_fwl_rules(key_value[1]) - - return get_rules_table(rules) - - -class FWEdit(environment.CLIRunnable): - """ -usage: sl firewall edit [options] - -Edit the rules for a firewall -""" - action = 'edit' - - def execute(self, args): - mgr = SoftLayer.FirewallManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - firewall_id = int(key_value[1]) - if key_value[0] == 'vlan': - orig_rules = mgr.get_dedicated_fwl_rules(firewall_id) - else: - orig_rules = mgr.get_standard_fwl_rules(firewall_id) - # open an editor for the user to enter their rules - edited_rules = open_editor(rules=orig_rules) - print(edited_rules) - if formatting.confirm("Would you like to submit the rules. " - "Continue?"): - while True: - try: - rules = parse_rules(edited_rules) - if key_value[0] == 'vlan': - rules = mgr.edit_dedicated_fwl_rules(firewall_id, - rules) - else: - rules = mgr.edit_standard_fwl_rules(firewall_id, - rules) - break - except (SoftLayer.SoftLayerError, ValueError) as error: - print("Unexpected error({%s})" % (error)) - if formatting.confirm("Would you like to continue editing " - "the rules. Continue?"): - edited_rules = open_editor(content=edited_rules) - print(edited_rules) - if formatting.confirm("Would you like to submit the " - "rules. Continue?"): - continue - else: - raise exceptions.CLIAbort('Aborted.') - else: - raise exceptions.CLIAbort('Aborted.') - return 'Firewall updated!' - else: - raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/modules/globalip.py b/SoftLayer/CLI/modules/globalip.py deleted file mode 100644 index 6f87c2a10..000000000 --- a/SoftLayer/CLI/modules/globalip.py +++ /dev/null @@ -1,170 +0,0 @@ -""" -usage: sl globalip [] [...] [options] - -Orders or configures global IP addresses - -The available commands are: - assign Assign a target to a global IP address - cancel Cancels a global IP - create Orders a new global IP address - list Display a list of global IP addresses - unassign Unassigns a global IP -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - - -class GlobalIpAssign(environment.CLIRunnable): - """ -usage: sl globalip assign [options] - -Assigns a global IP to a target. - -Required: - The ID or address of the global IP - The IP address to assign to the global IP -""" - action = 'assign' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, - args.get(''), - name='global ip') - mgr.assign_global_ip(global_ip_id, args['']) - - -class GlobalIpCancel(environment.CLIRunnable): - """ -usage: sl globalip cancel [options] - -Cancel a subnet -""" - - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, - args.get(''), - name='global ip') - - if args['--really'] or formatting.no_going_back(global_ip_id): - mgr.cancel_global_ip(global_ip_id) - else: - raise exceptions.CLIAbort('Aborted') - - -class GlobalIpCreate(environment.CLIRunnable): - """ -usage: - sl globalip create [options] - -Add a new global IP address to your account. - -Options: - --v6 Orders IPv6 - --test Do not order the IP; just get a quote -""" - action = 'create' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - - version = 4 - if args.get('--v6'): - version = 6 - if not args.get('--test') and not args['--really']: - if not formatting.confirm("This action will incur charges on your " - "account. Continue?"): - raise exceptions.CLIAbort('Cancelling order.') - result = mgr.add_global_ip(version=version, - test_order=args.get('--test')) - - table = formatting.Table(['Item', 'cost']) - table.align['Item'] = 'r' - table.align['cost'] = 'r' - - total = 0.0 - for price in result['orderDetails']['prices']: - total += float(price.get('recurringFee', 0.0)) - rate = "%.2f" % float(price['recurringFee']) - - table.add_row([price['item']['description'], rate]) - - table.add_row(['Total monthly cost', "%.2f" % total]) - return table - - -class GlobalIpList(environment.CLIRunnable): - """ -usage: sl globalip list [options] - -Displays a list of global IPs - -Filters: - --v4 Display only IPV4 - --v6 Display only IPV6 -""" - action = 'list' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - - table = formatting.Table(['id', 'ip', 'assigned', 'target']) - table.sortby = args.get('--sortby') or 'id' - - version = 0 - if args.get('--v4'): - version = 4 - elif args.get('--v6'): - version = 6 - - ips = mgr.list_global_ips(version=version) - - for ip_address in ips: - assigned = 'No' - target = 'None' - if ip_address.get('destinationIpAddress'): - dest = ip_address['destinationIpAddress'] - assigned = 'Yes' - target = dest['ipAddress'] - virtual_guest = dest.get('virtualGuest') - if virtual_guest: - target += (' (%s)' - % virtual_guest['fullyQualifiedDomainName']) - elif ip_address['destinationIpAddress'].get('hardware'): - target += (' (%s)' - % dest['hardware']['fullyQualifiedDomainName']) - - table.add_row([ip_address['id'], - ip_address['ipAddress']['ipAddress'], - assigned, - target]) - return table - - -class GlobalIpUnassign(environment.CLIRunnable): - """ -usage: sl globalip unassign [options] - -Unassigns a global IP from a target. - -Required: - The ID or address of the global IP -""" - action = 'unassign' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, - args.get(''), - name='global ip') - mgr.unassign_global_ip(global_ip_id) diff --git a/SoftLayer/CLI/modules/help.py b/SoftLayer/CLI/modules/help.py deleted file mode 100644 index 0866687e1..000000000 --- a/SoftLayer/CLI/modules/help.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -usage: sl help [options] - sl help [options] - sl help [options] - -View help on a module or command. -""" -# :license: MIT, see LICENSE for more details. -# Missing docstrings ignored due to __doc__ = __doc__ magic -# pylint: disable=C0111 - -from SoftLayer.CLI import core -from SoftLayer.CLI import environment - - -class Show(environment.CLIRunnable): - # Use the same documentation as the module - __doc__ = __doc__ - action = None - - def execute(self, args): - parser = core.CommandParser(self.env) - if not any([args[''], args['']]): - return parser.get_module_help('help') - - self.env.load_module(args['']) - - if args['']: - return parser.get_command_help(args[''], args['']) - elif args['']: - return parser.get_module_help(args['']) diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py deleted file mode 100644 index 5075eed60..000000000 --- a/SoftLayer/CLI/modules/image.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -usage: sl image [] [...] [options] - -Manage compute images - -The available commands are: - delete Delete an image - detail Output details about an image - list List images - edit Edit an image -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - -MASK = ('id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,' - 'imageType') -DETAIL_MASK = MASK + (',children[id,blockDevicesDiskSpaceTotal,datacenter],' - 'note,createDate,status') -PUBLIC_TYPE = formatting.FormattedItem('PUBLIC', 'Public') -PRIVATE_TYPE = formatting.FormattedItem('PRIVATE', 'Private') - - -class ListImages(environment.CLIRunnable): - """ -usage: sl image list [--public | --private] [options] - -List images - -Options: - --private Display only private images - --public Display only public images -""" - action = 'list' - - def execute(self, args): - image_mgr = SoftLayer.ImageManager(self.client) - - neither = not any([args['--private'], args['--public']]) - - images = [] - if args['--private'] or neither: - for image in image_mgr.list_private_images(mask=MASK): - images.append(image) - - if args['--public'] or neither: - for image in image_mgr.list_public_images(mask=MASK): - images.append(image) - - table = formatting.Table(['id', - 'account', - 'name', - 'type', - 'visibility', - 'global_identifier']) - - images = [image for image in images if image['parentId'] == ''] - for image in images: - - table.add_row([ - image['id'], - image.get('accountId', formatting.blank()), - image['name'].strip(), - formatting.FormattedItem( - utils.lookup(image, 'imageType', 'keyName'), - utils.lookup(image, 'imageType', 'name')), - PUBLIC_TYPE if image['publicFlag'] else PRIVATE_TYPE, - image.get('globalIdentifier', formatting.blank()), - ]) - - return table - - -class DetailImage(environment.CLIRunnable): - """ -usage: sl image detail [options] - -Get details for an image -""" - action = 'detail' - - def execute(self, args): - image_mgr = SoftLayer.ImageManager(self.client) - image_id = helpers.resolve_id(image_mgr.resolve_ids, - args.get(''), - 'image') - - image = image_mgr.get_image(image_id, mask=DETAIL_MASK) - disk_space = 0 - datacenters = [] - for child in image.get('children'): - disk_space = int(child.get('blockDevicesDiskSpaceTotal', 0)) - if child.get('datacenter'): - datacenters.append(utils.lookup(child, 'datacenter', 'name')) - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - table.add_row(['id', image['id']]) - table.add_row(['global_identifier', - image.get('globalIdentifier', formatting.blank())]) - table.add_row(['name', image['name'].strip()]) - table.add_row(['status', formatting.FormattedItem( - utils.lookup(image, 'status', 'keyname'), - utils.lookup(image, 'status', 'name'), - )]) - table.add_row(['account', image.get('accountId', formatting.blank())]) - table.add_row(['visibility', - PUBLIC_TYPE if image['publicFlag'] else PRIVATE_TYPE]) - table.add_row(['type', - formatting.FormattedItem( - utils.lookup(image, 'imageType', 'keyName'), - utils.lookup(image, 'imageType', 'name'), - )]) - table.add_row(['flex', image.get('flexImageFlag')]) - table.add_row(['note', image.get('note')]) - table.add_row(['created', image.get('createDate')]) - table.add_row(['disk_space', formatting.b_to_gb(disk_space)]) - table.add_row(['datacenters', formatting.listing(sorted(datacenters), - separator=',')]) - - return table - - -class DeleteImage(environment.CLIRunnable): - """ -usage: sl image delete [options] - -Get details for an image -""" - action = 'delete' - - def execute(self, args): - image_mgr = SoftLayer.ImageManager(self.client) - image_id = helpers.resolve_id(image_mgr.resolve_ids, - args.get(''), - 'image') - - image_mgr.delete_image(image_id) - - -class EditImage(environment.CLIRunnable): - """ -usage: sl image edit [--tag=Tag...] [options] - -Edit Details for an image - -Options: - --name=Name Name of the Image - --note=Note Note of the Image - --tag=TAG... Tags of the Image. Can be specified multiple times. - -Note: Image to be edited must be private -""" - action = 'edit' - - def execute(self, args): - image_mgr = SoftLayer.ImageManager(self.client) - data = {} - if args.get('--name'): - data['name'] = args.get('--name') - if args.get('--note'): - data['note'] = args.get('--note') - if args.get('--tag'): - data['tag'] = args.get('--tag') - image_id = helpers.resolve_id(image_mgr.resolve_ids, - args.get(''), 'image') - if not image_mgr.edit(image_id, **data): - raise exceptions.CLIAbort("Failed to Edit Image") diff --git a/SoftLayer/CLI/modules/iscsi.py b/SoftLayer/CLI/modules/iscsi.py deleted file mode 100644 index a0d5bbd41..000000000 --- a/SoftLayer/CLI/modules/iscsi.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -usage: sl iscsi [] [...] [options] - -Manage, order, delete iSCSI targets - -The available commands are: - cancel Cancel an existing iSCSI target - create Order and create an iSCSI target - detail Output details about an iSCSI - list List iSCSI targets on the account - -For several commands, will be asked for. This will be the id -for iSCSI target. -""" -# from SoftLayer.CLI import (CLIRunnable, Table, no_going_back, FormattedItem) -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - - -class ListISCSIs(environment.CLIRunnable): - - """ -usage: sl iscsi list [options] - -List iSCSI targets -""" - action = 'list' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - iscsi_list = iscsi_mgr.list_iscsi() - iscsi_list = [utils.NestedDict(n) for n in iscsi_list] - table = formatting.Table([ - 'id', - 'datacenter', - 'size', - 'username', - 'password', - 'server' - ]) - for iscsi in iscsi_list: - table.add_row([ - iscsi['id'], - iscsi['serviceResource']['datacenter'].get('name', - formatting.blank()), - formatting.FormattedItem( - iscsi.get('capacityGb', formatting.blank()), - "%dGB" % iscsi.get('capacityGb', 0)), - iscsi.get('username', formatting.blank()), - iscsi.get('password', formatting.blank()), - iscsi.get('serviceResourceBackendIpAddress', - formatting.blank())]) - return table - - -class CreateISCSI(environment.CLIRunnable): - - """ -usage: sl iscsi create [options] - -Orders and creates an iSCSI target. - -Examples: - sl iscsi create --size=1 --datacenter=dal05 - sl iscsi create --size 1 -d dal05 - sl iscsi create -s 1 -d dal05 - -Required: - -s, --size=SIZE Size of the iSCSI volume to create - -d, --datacenter=DC Datacenter shortname (sng01, dal05, ...) -""" - action = 'create' - options = ['confirm'] - required_params = ['--size', '--datacenter'] - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - self._validate_create_args(args) - size, location = self._parse_create_args(args) - iscsi_mgr.create_iscsi(size=size, location=location) - - def _parse_create_args(self, args): - """ Converts CLI arguments to arguments that can be passed into - ISCSIManager.create_iscsi. - :param dict args: CLI arguments - """ - size = args['--size'] - location = args['--datacenter'] - return int(size), str(location) - - def _validate_create_args(self, args): - """ Raises an ArgumentError if the given arguments are not valid """ - invalid_args = [k for k in self.required_params if args.get(k) is None] - if invalid_args: - raise exceptions.ArgumentError('Missing required options: %s' - % ','.join(invalid_args)) - - -class CancelISCSI(environment.CLIRunnable): - - """ -usage: sl iscsi cancel [options] - -Cancel existing iSCSI - -Examples: - sl iscsi cancel 12345 - sl iscsi cancel 12345 --immediate - sl iscsi cancel 12345 --immediate --reason='no longer needed' - -options : - --immediate Cancels the iSCSI immediately (instead of on the billing - anniversary) - --reason=REASON An optional reason for cancellation. -""" - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - iscsi_id = helpers.resolve_id( - iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - - immediate = args.get('--immediate', False) - - reason = args.get('--reason') - if args['--really'] or formatting.no_going_back(iscsi_id): - iscsi_mgr.cancel_iscsi(iscsi_id, reason, immediate) - else: - raise exceptions.CLIAbort('Aborted') - - -class ISCSIDetails(environment.CLIRunnable): - - """ -usage: sl iscsi detail [--password] [options] - -Get details for an iSCSI - -Examples: - sl iscsi detail 12345 - sl iscsi detail 12345 --password - -Options: - --password Show credentials to access the iSCSI target -""" - action = 'detail' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - iscsi_id = helpers.resolve_id( - iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - result = iscsi_mgr.get_iscsi(iscsi_id) - result = utils.NestedDict(result) - - table.add_row(['id', result['id']]) - table.add_row(['serviceResourceName', result['serviceResourceName']]) - table.add_row(['createDate', result['createDate']]) - table.add_row(['nasType', result['nasType']]) - table.add_row(['capacityGb', result['capacityGb']]) - if result['snapshotCapacityGb']: - table.add_row(['snapshotCapacityGb', result['snapshotCapacityGb']]) - table.add_row(['mountableFlag', result['mountableFlag']]) - table.add_row( - ['serviceResourceBackendIpAddress', - result['serviceResourceBackendIpAddress']]) - table.add_row(['price', result['billingItem']['recurringFee']]) - table.add_row(['BillingItemId', result['billingItem']['id']]) - if result.get('notes'): - table.add_row(['notes', result['notes']]) - - if args.get('--password'): - pass_table = formatting.Table(['username', 'password']) - pass_table.add_row([result['username'], result['password']]) - table.add_row(['users', pass_table]) - - return table diff --git a/SoftLayer/CLI/modules/loadbal.py b/SoftLayer/CLI/modules/loadbal.py deleted file mode 100755 index cd1e46c00..000000000 --- a/SoftLayer/CLI/modules/loadbal.py +++ /dev/null @@ -1,591 +0,0 @@ -""" -usage: sl loadbal [] [...] [options] - -Local LoadBalancer management - -The available commands are: - cancel Cancel an existing load balancer - create Create a new load balancer - create-options Lists the different packages for load balancers - detail Provide details about a particular load balancer - group-add Add a new service group in the load balancer - group-delete Delete a service group from the load balancer - group-edit Edit the properties of a service group - group-reset Resets all the connections on a service group - health-checks List the different health check values - list List active load balancers - routing-methods List supported routing methods - routing-types List supported routing types - service-add Add a service to an existing service group - service-delete Delete an existing service - service-edit Edit an existing service - service-toggle Toggle the status of the service -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - - -def get_ids(input_id): - """ Helper package to retrieve the actual IDs - - :param input_id: the ID provided by the user - :returns: A list of valid IDs - """ - key_value = input_id.split(':') - if len(key_value) != 2: - raise exceptions.CLIAbort( - 'Invalid ID %s: ID should be of the form xxx:yyy' % input_id) - return key_value - - -def get_local_lbs_table(load_balancers): - """ Helper package to format the local load balancers into a table. - - :param dict load_balancers: A dictionary representing the load_balancers - :returns: A table containing the local load balancers - """ - table = formatting.Table(['ID', - 'VIP Address', - 'Location', - 'SSL Offload', - 'Connections/second', - 'Type']) - - table.align['Connections/second'] = 'r' - - for load_balancer in load_balancers: - ssl_support = 'Not Supported' - if load_balancer['sslEnabledFlag']: - if load_balancer['sslActiveFlag']: - ssl_support = 'On' - else: - ssl_support = 'Off' - lb_type = 'Standard' - if load_balancer['dedicatedFlag']: - lb_type = 'Dedicated' - elif load_balancer['highAvailabilityFlag']: - lb_type = 'HA' - table.add_row([ - 'local:%s' % load_balancer['id'], - load_balancer['ipAddress']['ipAddress'], - load_balancer['loadBalancerHardware'][0]['datacenter']['name'], - ssl_support, - load_balancer['connectionLimit'], - lb_type - ]) - return table - - -def get_local_lb_table(load_balancer): - """ Helper package to format the local loadbal details into a table. - - :param dict load_balancer: A dictionary representing the loadbal - :returns: A table containing the local loadbal details - """ - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'l' - table.align['Value'] = 'l' - table.add_row(['General properties', '----------']) - table.add_row([' ID', 'local:%s' % load_balancer['id']]) - table.add_row([' IP Address', load_balancer['ipAddress']['ipAddress']]) - name = load_balancer['loadBalancerHardware'][0]['datacenter']['name'] - table.add_row([' Datacenter', name]) - table.add_row([' Connections limit', load_balancer['connectionLimit']]) - table.add_row([' Dedicated', load_balancer['dedicatedFlag']]) - table.add_row([' HA', load_balancer['highAvailabilityFlag']]) - table.add_row([' SSL Enabled', load_balancer['sslEnabledFlag']]) - table.add_row([' SSL Active', load_balancer['sslActiveFlag']]) - index0 = 1 - for virtual_server in load_balancer['virtualServers']: - table.add_row(['Service group %s' % index0, - '**************']) - index0 += 1 - table2 = formatting.Table(['Service group ID', - 'Port', - 'Allocation', - 'Routing type', - 'Routing Method']) - - for group in virtual_server['serviceGroups']: - table2.add_row([ - '%s:%s' % (load_balancer['id'], virtual_server['id']), - virtual_server['port'], - '%s %%' % virtual_server['allocation'], - '%s:%s' % (group['routingTypeId'], - group['routingType']['name']), - '%s:%s' % (group['routingMethodId'], - group['routingMethod']['name']) - ]) - - table.add_row([' Group Properties', table2]) - - table3 = formatting.Table(['Service_ID', - 'IP Address', - 'Port', - 'Health Check', - 'Weight', - 'Enabled', - 'Status']) - service_exist = False - for service in group['services']: - service_exist = True - health_check = service['healthChecks'][0] - table3.add_row([ - '%s:%s' % (load_balancer['id'], service['id']), - service['ipAddress']['ipAddress'], - service['port'], - '%s:%s' % (health_check['healthCheckTypeId'], - health_check['type']['name']), - service['groupReferences'][0]['weight'], - service['enabled'], - service['status'] - ]) - if service_exist: - table.add_row([' Services', table3]) - else: - table.add_row([' Services', 'None']) - return table - - -class LoadBalancerList(environment.CLIRunnable): - """ -usage: sl loadbal list [options] - -List active load balancers - -""" - action = 'list' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - load_balancers = mgr.get_local_lbs() - return get_local_lbs_table(load_balancers) - - -class LoadBalancerHealthChecks(environment.CLIRunnable): - """ -usage: sl loadbal health-checks [options] - -List load balancer service health check types that can be used -""" - action = 'health-checks' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - hc_types = mgr.get_hc_types() - table = formatting.KeyValueTable(['ID', 'Name']) - table.align['ID'] = 'l' - table.align['Name'] = 'l' - table.sortby = 'ID' - for hc_type in hc_types: - table.add_row([hc_type['id'], hc_type['name']]) - return table - - -class LoadBalancerRoutingMethods(environment.CLIRunnable): - """ -usage: sl loadbal routing-methods [options] - -List load balancers routing methods that can be used -""" - action = 'routing-methods' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - routing_methods = mgr.get_routing_methods() - table = formatting.KeyValueTable(['ID', 'Name']) - table.align['ID'] = 'l' - table.align['Name'] = 'l' - table.sortby = 'ID' - for routing_method in routing_methods: - table.add_row([routing_method['id'], routing_method['name']]) - return table - - -class LoadBalancerRoutingTypes(environment.CLIRunnable): - """ -usage: sl loadbal routing-types [options] - -List load balancers routing types that can be used -""" - action = 'routing-types' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - routing_types = mgr.get_routing_types() - table = formatting.KeyValueTable(['ID', 'Name']) - table.align['ID'] = 'l' - table.align['Name'] = 'l' - table.sortby = 'ID' - for routing_type in routing_types: - table.add_row([routing_type['id'], routing_type['name']]) - return table - - -class LoadBalancerDetails(environment.CLIRunnable): - """ -usage: sl loadbal detail [options] - -Get Load balancer details - -""" - action = 'detail' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - input_id = args.get('') - - key_value = get_ids(input_id) - loadbal_id = int(key_value[1]) - - load_balancer = mgr.get_local_lb(loadbal_id) - return get_local_lb_table(load_balancer) - - -class LoadBalancerCancel(environment.CLIRunnable): - """ -usage: sl loadbal cancel [options] - -Cancels an existing load_balancer - -""" - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - loadbal_id = int(key_value[1]) - - if args['--really'] or formatting.confirm("This action will cancel a " - "load balancer. Continue?"): - mgr.cancel_lb(loadbal_id) - return 'Load Balancer with id %s is being cancelled!' % input_id - else: - raise exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceDelete(environment.CLIRunnable): - """ -usage: sl loadbal service-delete [options] - -Deletes an existing load_balancer service - -""" - action = 'service-delete' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - service_id = int(key_value[1]) - - if args['--really'] or formatting.confirm("This action will cancel a " - "service from your load " - "balancer. Continue?"): - mgr.delete_service(service_id) - return 'Load balancer service %s is being cancelled!' % input_id - else: - raise exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceToggle(environment.CLIRunnable): - """ -usage: sl loadbal service-toggle [options] - -Toggle the status of an existing load_balancer service - -""" - action = 'service-toggle' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - service_id = int(key_value[1]) - - if args['--really'] or formatting.confirm("This action will toggle " - "the status on the service. " - "Continue?"): - mgr.toggle_service_status(service_id) - return 'Load balancer service %s status updated!' % input_id - else: - raise exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceEdit(environment.CLIRunnable): - """ -usage: sl loadbal service-edit [options] - -Enable an existing load_balancer service -Options: ---enabled=ENABLED Set to 1 to enable the service, or 0 to disable ---port=PORT Change the value of the port ---weight=WEIGHT Change the weight of the service ---hc_type=HCTYPE Change the health check type ---ip=IP Change the IP of the service - -""" - action = 'service-edit' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - loadbal_id = int(key_value[0]) - service_id = int(key_value[1]) - - # check if any input is provided - if not (args['--ip'] or args['--enabled'] or args['--weight'] - or args['--port'] or args['--hc_type']): - return 'At least one property is required to be changed!' - - # check if the IP is valid - ip_address_id = None - if args['--ip']: - ip_address = mgr.get_ip_address(args['--ip']) - if not ip_address: - return 'Provided IP address is not valid!' - else: - ip_address_id = ip_address['id'] - - mgr.edit_service(loadbal_id, - service_id, - ip_address_id=ip_address_id, - enabled=args.get('--enabled'), - port=args.get('--port'), - weight=args.get('--weight'), - hc_type=args.get('--hc_type')) - return 'Load balancer service %s is being modified!' % input_id - - -class LoadBalancerServiceAdd(environment.CLIRunnable): - """ -usage: sl loadbal service-add --ip=IP --port=PORT \ ---weight=WEIGHT --hc_type=HCTYPE --enabled=ENABLED [options] - -Adds a new load_balancer service -Required: ---enabled=ENABLED Set to 1 to enable the service, 0 to disable [default: 1]. ---port=PORT Set to the desired port value [default: 80]. ---weight=WEIGHT Set to the desired weight value [default: 1]. ---hc_type=HCTYPE Set to the desired health check value [default: 21]. ---ip=IP Set to the desired IP value. - -""" - action = 'service-add' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - loadbal_id = int(key_value[0]) - group_id = int(key_value[1]) - - # check if the IP is valid - ip_address = None - if args['--ip']: - ip_address = mgr.get_ip_address(args['--ip']) - if not ip_address: - return 'Provided IP address is not valid!' - - mgr.add_service(loadbal_id, - group_id, - ip_address_id=ip_address['id'], - enabled=args.get('--enabled'), - port=args.get('--port'), - weight=args.get('--weight'), - hc_type=args.get('--hc_type')) - return 'Load balancer service is being added!' - - -class LoadBalancerServiceGroupDelete(environment.CLIRunnable): - """ -usage: sl loadbal group-delete [options] - -Deletes an existing load_balancer service group - -""" - action = 'group-delete' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - group_id = int(key_value[1]) - - if args['--really'] or formatting.confirm("This action will cancel a " - "service group. Continue?"): - mgr.delete_service_group(group_id) - return 'Service group %s is being deleted!' % input_id - else: - raise exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceGroupEdit(environment.CLIRunnable): - """ -usage: sl loadbal group-edit [options] - -Edits an existing load_balancer service group -Options: ---allocation=PERC Change the allocated % of connections ---port=PORT Change the port ---routing_type=TYPE Change the port routing type ---routing_method=METHOD Change the routing method - -""" - action = 'group-edit' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - loadbal_id = int(key_value[0]) - group_id = int(key_value[1]) - - # check if any input is provided - if not (args['--allocation'] or args['--port'] - or args['--routing_type'] or args['--routing_method']): - return 'At least one property is required to be changed!' - - routing_type = args.get('--routing_type') - routing_method = args.get('--routing_method') - - mgr.edit_service_group(loadbal_id, - group_id, - allocation=args.get('--allocation'), - port=args.get('--port'), - routing_type=routing_type, - routing_method=routing_method) - - return 'Load balancer service group %s is being updated!' % input_id - - -class LoadBalancerServiceGroupReset(environment.CLIRunnable): - """ -usage: sl loadbal group-reset [options] - -Resets the connections on a certain service group - -""" - action = 'group-reset' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - loadbal_id = int(key_value[0]) - group_id = int(key_value[1]) - - mgr.reset_service_group(loadbal_id, group_id) - return 'Load balancer service group connections are being reset!' - - -class LoadBalancerServiceGroupAdd(environment.CLIRunnable): - """ -usage: sl loadbal group-add --allocation=PERC --port=PORT \ ---routing_type=TYPE --routing_method=METHOD [options] - -Adds a new load_balancer service -Required: ---allocation=PERC The % of connections that will be allocated ---port=PORT The virtual port number for the group ---routing_type=TYPE The routing type for the group ---routing_method=METHOD The routing method for the group - -""" - action = 'group-add' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - key_value = get_ids(input_id) - - loadbal_id = int(key_value[1]) - - mgr.add_service_group(loadbal_id, - allocation=int(args.get('--allocation')), - port=int(args.get('--port')), - routing_type=int(args.get('--routing_type')), - routing_method=int(args.get('--routing_method'))) - - return 'Load balancer service group is being added!' - - -class LoadBalancerCreate(environment.CLIRunnable): - """ -usage: sl loadbal create (--datacenter=DC) [options] - -Adds a load_balancer given the billing id returned from create-options - -Options: - -d, --datacenter=DC Datacenter shortname (sng01, dal05, ...) - Note: Omitting this value defaults to the first - available datacenter -""" - action = 'create' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = helpers.resolve_id( - mgr.resolve_ids, args.get(''), 'load_balancer') - if not formatting.confirm("This action will incur charges on your " - "account. Continue?"): - raise exceptions.CLIAbort('Aborted.') - mgr.add_local_lb(input_id, datacenter=args['--datacenter']) - return "Load balancer is being created!" - - -class CreateOptionsLoadBalancer(environment.CLIRunnable): - """ -usage: sl loadbal create-options - -Output available options when adding a new load balancer - -""" - action = 'create-options' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - table = formatting.Table(['id', 'capacity', 'description', 'price']) - - table.sortby = 'price' - table.align['price'] = 'r' - table.align['capacity'] = 'r' - table.align['id'] = 'r' - - packages = mgr.get_lb_pkgs() - - for package in packages: - table.add_row([ - package['prices'][0]['id'], - package.get('capacity'), - package['description'], - format(float(package['prices'][0]['recurringFee']), '.2f') - ]) - - return table diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py deleted file mode 100644 index 0c39a8ad0..000000000 --- a/SoftLayer/CLI/modules/messaging.py +++ /dev/null @@ -1,522 +0,0 @@ -""" -usage: sl messaging [] [...] [options] - -Manage the SoftLayer Message Queue service. For most commands, a queue account -is required. Use 'sl messaging accounts-list' to list current accounts - -The available commands are: - accounts-list List all queue accounts - endpoints-list List all service endpoints - ping Ping the service - - queue-add Create a new queue - queue-detail Prints the details of a queue - queue-edit Modifies an existing queue - queue-list Lists out all queues on an account - queue-pop Pop a message from a queue - queue-push Pushes a message into a queue - queue-remove Delete a queue - - topic-add Creates a new topic - topic-detail Prints the details of a topic - topic-list Lists out all topics on an account - topic-push Pushes a notification to a topic - topic-remove Deletes a topic - topic-subscribe Adds a subscription on a topic - topic-unsubscribe Remove a subscription on a topic - -""" -# :license: MIT, see LICENSE for more details. -# Missing docstrings ignored due to __doc__ = __doc__ magic -# pylint: disable=C0111 -import sys - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -COMMON_MESSAGING_ARGS = """Service Options: - --datacenter=NAME Datacenter, E.G.: dal05 - --network=TYPE Network type, [Options: public, private] -""" - - -class ListAccounts(environment.CLIRunnable): - """ -usage: sl messaging accounts-list [options] - -List SoftLayer Message Queue Accounts - -""" - action = 'accounts-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - accounts = manager.list_accounts() - - table = formatting.Table([ - 'id', 'name', 'status' - ]) - for account in accounts: - if not account['nodes']: - continue - - table.add_row([ - account['nodes'][0]['accountName'], - account['name'], - account['status']['name'], - ]) - - return table - - -class ListEndpoints(environment.CLIRunnable): - """ -usage: sl messaging endpoints-list [options] - -List SoftLayer Message Queue Endpoints - -""" - action = 'endpoints-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - regions = manager.get_endpoints() - - table = formatting.Table([ - 'name', 'public', 'private' - ]) - for region, endpoints in regions.items(): - table.add_row([ - region, - endpoints.get('public') or formatting.blank(), - endpoints.get('private') or formatting.blank(), - ]) - - return table - - -class Ping(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging ping [options] - -Ping the SoftLayer Message Queue service - -""" + COMMON_MESSAGING_ARGS - action = 'ping' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - okay = manager.ping( - datacenter=args['--datacenter'], network=args['--network']) - if okay: - return 'OK' - else: - exceptions.CLIAbort('Ping failed') - - -def queue_table(queue): - """ Returns a table with details about a queue """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['name', queue['name']]) - table.add_row(['message_count', queue['message_count']]) - table.add_row(['visible_message_count', queue['visible_message_count']]) - table.add_row(['tags', formatting.listing(queue['tags'] or [])]) - table.add_row(['expiration', queue['expiration']]) - table.add_row(['visibility_interval', queue['visibility_interval']]) - return table - - -def message_table(message): - """ Returns a table with details about a message """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['id', message['id']]) - table.add_row(['initial_entry_time', message['initial_entry_time']]) - table.add_row(['visibility_delay', message['visibility_delay']]) - table.add_row(['visibility_interval', message['visibility_interval']]) - table.add_row(['fields', message['fields']]) - return [table, message['body']] - - -def topic_table(topic): - """ Returns a table with details about a topic """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['name', topic['name']]) - table.add_row(['tags', formatting.listing(topic['tags'] or [])]) - return table - - -def subscription_table(sub): - """ Returns a table with details about a subscription """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['id', sub['id']]) - table.add_row(['endpoint_type', sub['endpoint_type']]) - for key, val in sub['endpoint'].items(): - table.add_row([key, val]) - return table - - -class QueueList(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-list [options] - -List all queues on an account - -""" + COMMON_MESSAGING_ARGS - action = 'queue-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - queues = mq_client.get_queues()['items'] - - table = formatting.Table([ - 'name', 'message_count', 'visible_message_count' - ]) - for queue in queues: - table.add_row([ - queue['name'], - queue['message_count'], - queue['visible_message_count'], - ]) - return table - - -class QueueDetail(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-detail [options] - -Detail a queue - -""" + COMMON_MESSAGING_ARGS - action = 'queue-detail' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - queue = mq_client.get_queue(args['']) - return queue_table(queue) - - -class QueueCreate(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-add [options] - -Create a queue - -Options: - --visibility_interval=SECONDS Time in seconds that messages will re-appear - after being popped - --expiration=SECONDS Time in seconds that messages will live - --tags=TAGS Comma-separated list of tags - -""" + COMMON_MESSAGING_ARGS - action = 'queue-add' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - queue = mq_client.create_queue( - args[''], - visibility_interval=int(args.get('--visibility_interval') or 30), - expiration=int(args.get('--expiration') or 604800), - tags=tags, - ) - return queue_table(queue) - - -class QueueModify(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-edit [options] - -Modify a queue - -Options: - --visibility_interval=SECONDS Time in seconds that messages will re-appear - after being popped - --expiration=SECONDS Time in seconds that messages will live - --tags=TAGS Comma-separated list of tags - -""" + COMMON_MESSAGING_ARGS - action = 'queue-edit' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - queue = mq_client.create_queue( - args[''], - visibility_interval=int(args.get('--visibility_interval') or 30), - expiration=int(args.get('--expiration') or 604800), - tags=tags, - ) - return queue_table(queue) - - -class QueueDelete(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-remove [] - [options] - -Delete a queue or a queued message - -Options: - --force Flag to force the deletion of the queue even when there are messages - -""" + COMMON_MESSAGING_ARGS - action = 'queue-remove' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - if args['']: - mq_client.delete_message(args[''], - args['']) - else: - mq_client.delete_queue(args[''], args.get('--force')) - - -class QueuePush(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-push ( | -) - [options] - -Push a message into a queue - -Options: - --force Flag to force the deletion of the queue even when there are messages - -""" + COMMON_MESSAGING_ARGS - action = 'queue-push' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - body = '' - if args[''] == '-': - body = sys.stdin.read() - else: - body = args[''] - return message_table( - mq_client.push_queue_message(args[''], body)) - - -class QueuePop(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-pop [options] - -Pops a message from a queue - -Options: - --count=NUM Count of messages to pop - --delete-after Remove popped messages from the queue - -""" + COMMON_MESSAGING_ARGS - action = 'queue-pop' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - messages = mq_client.pop_messages( - args[''], - args.get('--count') or 1) - formatted_messages = [] - for message in messages['items']: - formatted_messages.append(message_table(message)) - - if args.get('--delete-after'): - for message in messages['items']: - mq_client.delete_message( - args[''], - message['id']) - return formatted_messages - - -class TopicList(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-list [options] - -List all topics on an account - -""" + COMMON_MESSAGING_ARGS - action = 'topic-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - topics = mq_client.get_topics()['items'] - - table = formatting.Table(['name']) - for topic in topics: - table.add_row([topic['name']]) - return table - - -class TopicDetail(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-detail [options] - -Detail a topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-detail' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - topic = mq_client.get_topic(args['']) - subscriptions = mq_client.get_subscriptions(args['']) - tables = [] - for sub in subscriptions['items']: - tables.append(subscription_table(sub)) - return [topic_table(topic), tables] - - -class TopicCreate(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-add [options] - -Create a new topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-add' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - topic = mq_client.create_topic( - args[''], - visibility_interval=int( - args.get('--visibility_interval') or 30), - expiration=int(args.get('--expiration') or 604800), - tags=tags, - ) - return topic_table(topic) - - -class TopicDelete(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-remove [options] - -Delete a topic or subscription - -Options: - --force Flag to force the deletion of the topic even when there are - subscriptions -""" + COMMON_MESSAGING_ARGS - action = 'topic-remove' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - mq_client.delete_topic(args[''], args.get('--force')) - - -class TopicSubscribe(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-subscribe [options] - -Create a subscription on a topic - -Options: - --type=TYPE Type of endpoint, [Options: http, queue] - --queue-name=NAME Queue name. Required if --type is queue - --http-method=METHOD HTTP Method to use if --type is http - --http-url=URL HTTP/HTTPS URL to use. Required if --type is http - --http-body=BODY HTTP Body template to use if --type is http - -""" + COMMON_MESSAGING_ARGS - action = 'topic-subscribe' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - if args['--type'] == 'queue': - subscription = mq_client.create_subscription( - args[''], - 'queue', - queue_name=args['--queue-name'], - ) - elif args['--type'] == 'http': - subscription = mq_client.create_subscription( - args[''], - 'http', - method=args['--http-method'] or 'GET', - url=args['--http-url'], - body=args['--http-body'] - ) - else: - raise exceptions.ArgumentError( - '--type should be either queue or http.') - return subscription_table(subscription) - - -class TopicUnsubscribe(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-unsubscribe - [options] - -Remove a subscription on a topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-unsubscribe' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - mq_client.delete_subscription( - args[''], - args['']) - - -class TopicPush(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-push ( | -) - [options] - -Push a message into a topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-push' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - # the message body comes from the positional argument or stdin - body = '' - if args[''] == '-': - body = sys.stdin.read() - else: - body = args[''] - return message_table( - mq_client.push_topic_message(args[''], body)) diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py deleted file mode 100644 index 4bf89d9b4..000000000 --- a/SoftLayer/CLI/modules/metadata.py +++ /dev/null @@ -1,243 +0,0 @@ -""" -usage: sl metadata [] [...] [options] - -Find details about this machine. These commands only work on devices on the -backend SoftLayer network. This allows for self-discovery for newly provisioned -resources. - -The available commands are: - backend_ip Primary backend ip address - backend_mac Backend mac addresses - datacenter Datacenter name - datacenter_id Datacenter id - fqdn Fully qualified domain name - frontend_mac Frontend mac addresses - hostname Hostname - id Id - ip Primary ip address - network Details about either the public or private network - provision_state Provision state - tags Tags - user_data User-defined data -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -class MetaRunnable(environment.CLIRunnable): - """ A CLIRunnable that raises a nice error on connection issues because - the metadata service is only accessable on a SoftLayer device - """ - def execute(self, args): - try: - return self._execute(args) - except SoftLayer.TransportError: - raise exceptions.CLIAbort( - 'Cannot connect to the backend service address. Make sure ' - 'this command is being ran from a device on the backend ' - 'network.') - - def _execute(self, _): - """ To be overridden exactly like the execute() method """ - pass - - -class BackendMacAddresses(MetaRunnable): - """ -usage: sl metadata backend_mac [options] - -List backend mac addresses -""" - action = 'backend_mac' - - def _execute(self, _): - backend_macs = SoftLayer.MetadataManager().get('backend_mac') - return formatting.listing(backend_macs, separator=',') - - -class Datacenter(MetaRunnable): - """ -usage: sl metadata datacenter [options] - -Get datacenter name -""" - action = 'datacenter' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('datacenter') - - -class DatacenterId(MetaRunnable): - """ -usage: sl metadata datacenter_id [options] - -Get datacenter id -""" - action = 'datacenter_id' - - def _execute(self, _): - return str(SoftLayer.MetadataManager().get('datacenter_id')) - - -class FrontendMacAddresses(MetaRunnable): - """ -usage: sl metadata frontend_mac [options] - -List frontend mac addresses -""" - action = 'frontend_mac' - - def _execute(self, _): - frontend_macs = SoftLayer.MetadataManager().get('frontend_mac') - return formatting.listing(frontend_macs, separator=',') - - -class FullyQualifiedDomainName(MetaRunnable): - """ -usage: sl metadata fqdn [options] - -Get fully qualified domain name -""" - action = 'fqdn' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('fqdn') - - -class Hostname(MetaRunnable): - """ -usage: sl metadata hostname [options] - -Get hostname -""" - action = 'hostname' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('hostname') - - -class Id(MetaRunnable): - """ -usage: sl metadata id - -Get id -""" - action = 'id' - - def _execute(self, _): - return str(SoftLayer.MetadataManager().get('id')) - - -class PrimaryBackendIpAddress(MetaRunnable): - """ -usage: sl metadata backend_ip [options] - -Get primary backend ip address -""" - action = 'backend_ip' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('primary_backend_ip') - - -class PrimaryIpAddress(MetaRunnable): - """ -usage: sl metadata ip [options] - -Get primary ip address -""" - action = 'ip' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('primary_ip') - - -class ProvisionState(MetaRunnable): - """ -usage: sl metadata provision_state [options] - -Get provision state -""" - action = 'provision_state' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('provision_state') - - -class Tags(MetaRunnable): - """ -usage: sl metadata tags [options] - -List tags -""" - action = 'tags' - - def _execute(self, _): - return formatting.listing(SoftLayer.MetadataManager().get('tags'), - separator=',') - - -class UserMetadata(MetaRunnable): - """ -usage: sl metadata user_data [options] - -Get user-defined data -""" - action = 'user_data' - - def _execute(self, _): - """ Returns user metadata """ - userdata = SoftLayer.MetadataManager().get('user_data') - if userdata: - return userdata - else: - raise exceptions.CLIAbort("No user metadata.") - - -class Network(MetaRunnable): - """ -usage: sl metadata network ( | ) [options] - -Get details about the public or private network -""" - action = 'network' - - def _execute(self, args): - meta = SoftLayer.MetadataManager() - if args['']: - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - network = meta.public_network() - table.add_row([ - 'mac addresses', - formatting.listing(network['mac_addresses'], separator=',')]) - table.add_row([ - 'router', network['router']]) - table.add_row([ - 'vlans', formatting.listing(network['vlans'], separator=',')]) - table.add_row([ - 'vlan ids', - formatting.listing(network['vlan_ids'], separator=',')]) - return table - - if args['']: - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - network = meta.private_network() - table.add_row([ - 'mac addresses', - formatting.listing(network['mac_addresses'], separator=',')]) - table.add_row([ - 'router', network['router']]) - table.add_row([ - 'vlans', formatting.listing(network['vlans'], separator=',')]) - table.add_row([ - 'vlan ids', - formatting.listing(network['vlan_ids'], separator=',')]) - return table diff --git a/SoftLayer/CLI/modules/nas.py b/SoftLayer/CLI/modules/nas.py deleted file mode 100644 index 860e7bf09..000000000 --- a/SoftLayer/CLI/modules/nas.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -usage: sl nas [] [...] [options] - -Manage NAS accounts - -The available commands are: - list List NAS accounts -""" -# :license: MIT, see LICENSE for more details. - -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer import utils - - -class ListNAS(environment.CLIRunnable): - """ -usage: sl nas list [options] - -List NAS accounts - -Options: -""" - action = 'list' - - def execute(self, args): - account = self.client['Account'] - - nas_accounts = account.getNasNetworkStorage( - mask='eventCount,serviceResource[datacenter.name]') - - table = formatting.Table(['id', 'datacenter', 'size', 'username', - 'password', 'server']) - - for nas_account in nas_accounts: - table.add_row([ - nas_account['id'], - utils.lookup(nas_account, - 'serviceResource', - 'datacenter', - 'name') or formatting.blank(), - formatting.FormattedItem( - nas_account.get('capacityGb', formatting.blank()), - "%dGB" % nas_account.get('capacityGb', 0)), - nas_account.get('username', formatting.blank()), - nas_account.get('password', formatting.blank()), - nas_account.get('serviceResourceBackendIpAddress', - formatting.blank())]) - - return table diff --git a/SoftLayer/CLI/modules/rwhois.py b/SoftLayer/CLI/modules/rwhois.py deleted file mode 100644 index c036488a0..000000000 --- a/SoftLayer/CLI/modules/rwhois.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -usage: sl rwhois [] [...] [options] - -Manage the RWhoIs information on the account. - -The available commands are: - edit Edit the RWhois data on the account - show Show the RWhois data on the account -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -class RWhoisEdit(environment.CLIRunnable): - """ -usage: sl rwhois edit [options] - -Updates the RWhois information on your account. Only the fields you -specify will be changed. To clear a value, specify an empty string like: "" - -Options: - --abuse=EMAIL Set the abuse email - --address1=ADDR Update the address 1 field - --address2=ADDR Update the address 2 field - --city=CITY Set the city information - --company=NAME Set the company name - --country=COUNTRY Set the country information. Use the two-letter - abbreviation. - --firstname=NAME Update the first name field - --lastname=NAME Update the last name field - --postal=CODE Set the postal code field - --private Flags the address as a private residence. - --public Flags the address as a public residence. - --state=STATE Set the state information. Use the two-letter - abbreviation. -""" - action = 'edit' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - - update = { - 'abuse_email': args.get('--abuse'), - 'address1': args.get('--address1'), - 'address2': args.get('--address2'), - 'company_name': args.get('--company'), - 'city': args.get('--city'), - 'country': args.get('--country'), - 'first_name': args.get('--firstname'), - 'last_name': args.get('--lastname'), - 'postal_code': args.get('--postal'), - 'state': args.get('--state') - } - - if args.get('--private'): - update['private_residence'] = False - elif args.get('--public'): - update['private_residence'] = True - - check = [x for x in update.values() if x is not None] - if not check: - raise exceptions.CLIAbort( - "You must specify at least one field to update.") - - mgr.edit_rwhois(**update) # pylint: disable=W0142 - - -class RWhoisShow(environment.CLIRunnable): - """ -usage: sl rwhois show [options] - -Display the RWhois information for your account. -""" - action = 'show' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - result = mgr.get_rwhois() - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - table.add_row(['Name', result['firstName'] + ' ' + result['lastName']]) - table.add_row(['Company', result['companyName']]) - table.add_row(['Abuse Email', result['abuseEmail']]) - table.add_row(['Address 1', result['address1']]) - if result.get('address2'): - table.add_row(['Address 2', result['address2']]) - table.add_row(['City', result['city']]) - table.add_row(['State', result.get('state', '-')]) - table.add_row(['Postal Code', result.get('postalCode', '-')]) - table.add_row(['Country', result['country']]) - - return table diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py deleted file mode 100644 index e49891991..000000000 --- a/SoftLayer/CLI/modules/server.py +++ /dev/null @@ -1,1107 +0,0 @@ -""" -usage: sl server [] [...] [options] - sl server [-h | --help] - -Manage hardware servers - -The available commands are: - cancel Cancel a dedicated server. - cancel-reasons Provides the list of possible cancellation reasons - create Create a new dedicated server - create-options Display a list of creation options for a specific chassis - detail Retrieve hardware details - list List hardware devices - list-chassis Provide a list of all chassis available for ordering - nic-edit Edit NIC settings - power-cycle Issues power cycle to server - power-off Powers off a running server - power-on Boots up a server - reboot Reboots a running server - reload Perform an OS reload - -For several commands, will be asked for. This can be the id, -hostname or the ip address for a piece of hardware. -""" -# :license: MIT, see LICENSE for more details. -import os -import re - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer.CLI import template -from SoftLayer import utils - - -class ListServers(environment.CLIRunnable): - """ -usage: sl server list [options] - -List hardware servers on the account - -Examples: - sl server list --datacenter=dal05 - sl server list --network=100 --domain=example.com - sl server list --tags=production,db - -Options: - --sortby=ARG Column to sort by. options: id, datacenter, host, cores, - memory, primary_ip, backend_ip - -Filters: - -c, --cpu=CPU Number of CPU cores - -D, --domain=DOMAIN Domain portion of the FQDN. example: example.com - -d, --datacenter=DC Datacenter shortname (sng01, dal05, ...) - -H, --hostname=HOST Host portion of the FQDN. example: server - -m, --memory=MEMORY Memory in gigabytes - -n, --network=MBPS Network port speed in Mbps - --tags=ARG Only show instances that have one of these tags. - Comma-separated. (production,db) - -For more on filters see 'sl help filters' -""" - action = 'list' - - def execute(self, args): - manager = SoftLayer.HardwareManager(self.client) - - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - servers = manager.list_hardware( - hostname=args.get('--hostname'), - domain=args.get('--domain'), - cpus=args.get('--cpu'), - memory=args.get('--memory'), - datacenter=args.get('--datacenter'), - nic_speed=args.get('--network'), - tags=tags) - - table = formatting.Table([ - 'id', - 'datacenter', - 'host', - 'cores', - 'memory', - 'primary_ip', - 'backend_ip', - 'active_transaction', - 'owner' - ]) - table.sortby = args.get('--sortby') or 'host' - - for server in servers: - server = utils.NestedDict(server) - - table.add_row([ - server['id'], - server['datacenter']['name'] or formatting.blank(), - server['fullyQualifiedDomainName'], - server['processorPhysicalCoreAmount'], - formatting.gb(server['memoryCapacity'] or 0), - server['primaryIpAddress'] or formatting.blank(), - server['primaryBackendIpAddress'] or formatting.blank(), - formatting.active_txn(server), - utils.lookup( - server, 'billingItem', 'orderItem', 'order', 'userRecord', - 'username') or formatting.blank(), - ]) - - return table - - -class ServerDetails(environment.CLIRunnable): - """ -usage: sl server detail [--passwords] [--price] [options] - -Get details for a hardware device - -Options: - --passwords Show passwords (check over your shoulder!) - --price Show associated prices -""" - action = 'detail' - - def execute(self, args): - hardware = SoftLayer.HardwareManager(self.client) - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - hardware_id = helpers.resolve_id( - hardware.resolve_ids, args.get(''), 'hardware') - result = hardware.get_hardware(hardware_id) - result = utils.NestedDict(result) - - table.add_row(['id', result['id']]) - table.add_row(['hostname', result['fullyQualifiedDomainName']]) - table.add_row(['status', result['hardwareStatus']['status']]) - table.add_row(['datacenter', - result['datacenter']['name'] or formatting.blank()]) - table.add_row(['cores', result['processorPhysicalCoreAmount']]) - table.add_row(['memory', - formatting.gb(result['memoryCapacity'])]) - table.add_row(['public_ip', - result['primaryIpAddress'] or formatting.blank()]) - table.add_row(['private_ip', - result['primaryBackendIpAddress'] - or formatting.blank()]) - table.add_row(['ipmi_ip', - result['networkManagementIpAddress'] - or formatting.blank()]) - table.add_row([ - 'os', - formatting.FormattedItem( - result['operatingSystem']['softwareLicense'] - ['softwareDescription']['referenceCode'] or formatting.blank(), - result['operatingSystem']['softwareLicense'] - ['softwareDescription']['name'] or formatting.blank() - )]) - - table.add_row( - ['created', result['provisionDate'] or formatting.blank()]) - - table.add_row(['owner', formatting.FormattedItem( - utils.lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', - 'username') or formatting.blank() - )]) - - vlan_table = formatting.Table(['type', 'number', 'id']) - - for vlan in result['networkVlans']: - vlan_table.add_row([ - vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) - table.add_row(['vlans', vlan_table]) - - if result.get('notes'): - table.add_row(['notes', result['notes']]) - - if args.get('--price'): - table.add_row(['price rate', - result['billingItem']['recurringFee']]) - - if args.get('--passwords'): - user_strs = [] - for item in result['operatingSystem']['passwords']: - user_strs.append( - "%s %s" % (item['username'], item['password'])) - table.add_row(['users', formatting.listing(user_strs)]) - - tag_row = [] - for tag in result['tagReferences']: - tag_row.append(tag['tag']['name']) - - if tag_row: - table.add_row(['tags', formatting.listing(tag_row, separator=',')]) - - # Test to see if this actually has a primary (public) ip address - try: - if not result['privateNetworkOnlyFlag']: - ptr_domains = (self.client['Hardware_Server'] - .getReverseDomainRecords(id=hardware_id)) - - for ptr_domain in ptr_domains: - for ptr in ptr_domain['resourceRecords']: - table.add_row(['ptr', ptr['data']]) - except SoftLayer.SoftLayerAPIError: - pass - return table - - -class ServerReload(environment.CLIRunnable): - """ -usage: sl server reload [--key=KEY...] [options] - -Reload the OS on a hardware server based on its current configuration - -Optional: - -i, --postinstall=URI Post-install script to download - (Only HTTPS executes, HTTP leaves file in /root) - -k, --key=KEY SSH keys to add to the root user. Can be specified - multiple times -""" - - action = 'reload' - options = ['confirm'] - - def execute(self, args): - hardware = SoftLayer.HardwareManager(self.client) - hardware_id = helpers.resolve_id( - hardware.resolve_ids, args.get(''), 'hardware') - keys = [] - if args.get('--key'): - for key in args.get('--key'): - resolver = SoftLayer.SshKeyManager(self.client).resolve_ids - key_id = helpers.resolve_id(resolver, key, 'SshKey') - keys.append(key_id) - if args['--really'] or formatting.no_going_back(hardware_id): - hardware.reload(hardware_id, args['--postinstall'], keys) - else: - raise exceptions.CLIAbort('Aborted') - - -class CancelServer(environment.CLIRunnable): - """ -usage: sl server cancel [options] - -Cancel a dedicated server - -Options: - --immediate Cancels the server immediately (instead of on the billing - anniversary) - --comment=COMMENT An optional comment to add to the cancellation ticket - --reason=REASON An optional cancellation reason. See cancel-reasons for a - list of available options -""" - - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id( - mgr.resolve_ids, args.get(''), 'hardware') - - comment = args.get('--comment') - - if not comment and not args['--really']: - comment = self.env.input("(Optional) Add a cancellation comment:") - - reason = args.get('--reason') - immediate = args.get('--immediate') - - if args['--really'] or formatting.no_going_back(hw_id): - mgr.cancel_hardware(hw_id, reason, comment, immediate) - else: - raise exceptions.CLIAbort('Aborted') - - -class ServerCancelReasons(environment.CLIRunnable): - """ -usage: sl server cancel-reasons - -Display a list of cancellation reasons -""" - - action = 'cancel-reasons' - - def execute(self, args): - table = formatting.Table(['Code', 'Reason']) - table.align['Code'] = 'r' - table.align['Reason'] = 'l' - - mgr = SoftLayer.HardwareManager(self.client) - - for code, reason in mgr.get_cancellation_reasons().items(): - table.add_row([code, reason]) - - return table - - -class ServerPowerOff(environment.CLIRunnable): - """ -usage: sl server power-off [options] - -Power off an active server -""" - action = 'power-off' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - if args['--really'] or formatting.confirm('This will power off the ' - 'server with id %s ' - 'Continue?' % hw_id): - self.client['Hardware_Server'].powerOff(id=hw_id) - else: - raise exceptions.CLIAbort('Aborted.') - - -class ServerReboot(environment.CLIRunnable): - """ -usage: sl server reboot [--hard | --soft] [options] - -Reboot an active server - -Optional: - --hard Perform an abrupt reboot - --soft Perform a graceful reboot -""" - action = 'reboot' - options = ['confirm'] - - def execute(self, args): - hardware_server = self.client['Hardware_Server'] - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - if args['--really'] or formatting.confirm('This will power off the ' - 'server with id %s. ' - 'Continue?' % hw_id): - if args['--hard']: - hardware_server.rebootHard(id=hw_id) - elif args['--soft']: - hardware_server.rebootSoft(id=hw_id) - else: - hardware_server.rebootDefault(id=hw_id) - else: - raise exceptions.CLIAbort('Aborted.') - - -class ServerPowerOn(environment.CLIRunnable): - """ -usage: sl server power-on [options] - -Power on a server -""" - action = 'power-on' - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - self.client['Hardware_Server'].powerOn(id=hw_id) - - -class ServerPowerCycle(environment.CLIRunnable): - """ -usage: sl server power-cycle [options] - -Issues power cycle to server via the power strip -""" - action = 'power-cycle' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - - if args['--really'] or formatting.confirm('This will power off the ' - 'server with id %s. ' - 'Continue?' % hw_id): - self.client['Hardware_Server'].powerCycle(id=hw_id) - else: - raise exceptions.CLIAbort('Aborted.') - - -class NicEditServer(environment.CLIRunnable): - """ -usage: sl server nic-edit (public | private) --speed=SPEED - [options] - -Manage NIC settings - -Options: - --speed=SPEED Port speed. 0 disables the port. - [Options: 0, 10, 100, 1000, 10000] -""" - action = 'nic-edit' - - def execute(self, args): - public = args['public'] - - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - - mgr.change_port_speed(hw_id, public, args['--speed']) - - -class ListChassisServer(environment.CLIRunnable): - """ -usage: sl server list-chassis [options] - -Display a list of chassis available for ordering dedicated servers. -""" - action = 'list-chassis' - - def execute(self, args): - table = formatting.Table(['Code', 'Chassis']) - table.align['Code'] = 'r' - table.align['Chassis'] = 'l' - - mgr = SoftLayer.HardwareManager(self.client) - chassis = mgr.get_available_dedicated_server_packages() - - for chassis in chassis: - table.add_row([chassis[0], chassis[1]]) - - return table - - -class ServerRescue(environment.CLIRunnable): - """ -usage: sl server rescue [options] - -Reboot server into a rescue image - - -""" - action = 'rescue' - options = ['confirm'] - - def execute(self, args): - server = SoftLayer.HardwareManager(self.client) - server_id = helpers.resolve_id(server.resolve_ids, - args.get(''), - 'hardware') - - if args['--really'] or formatting.confirm( - "This action will reboot this server. " - "Continue?"): - - server.rescue(server_id) - else: - raise exceptions.CLIAbort('Aborted') - - -class ServerCreateOptions(environment.CLIRunnable): - """ -usage: sl server create-options [options] - -Output available available options when creating a dedicated server with the -specified chassis. - -Options: - --all Show all options. default if no other option provided - --controller Show disk controller options - --cpu Show CPU options - --datacenter Show datacenter options - --disk Show disk options - --memory Show memory size options - --nic Show NIC speed options - --os Show operating system options -""" - - action = 'create-options' - options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', - 'controller'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - chassis_id = args.get('') - - found = False - for chassis in mgr.get_available_dedicated_server_packages(): - if chassis_id == str(chassis[0]): - found = True - break - - if not found: - raise exceptions.CLIAbort('Invalid chassis specified.') - - ds_options = mgr.get_dedicated_server_create_options(chassis_id) - - show_all = True - for opt_name in self.options: - if args.get("--" + opt_name): - show_all = False - break - - if args['--all']: - show_all = True - - # Determine if this is a "Bare Metal Instance" or regular server - bmc = False - if chassis_id == str(mgr.get_bare_metal_package_id()): - bmc = True - - if args['--datacenter'] or show_all: - results = self.get_create_options(ds_options, 'datacenter')[0] - - table.add_row([results[0], formatting.listing(sorted(results[1]))]) - - if (args['--cpu'] or show_all) and not bmc: - results = self.get_create_options(ds_options, 'cpu') - - cpu_table = formatting.Table(['ID', 'Description']) - cpu_table.align['ID'] = 'r' - cpu_table.align['Description'] = 'l' - - for result in sorted(results, key=lambda x: x[1]): - cpu_table.add_row([result[1], result[0]]) - table.add_row(['cpu', cpu_table]) - - if (args['--memory'] or show_all) and not bmc: - results = self.get_create_options(ds_options, 'memory')[0] - - table.add_row([results[0], formatting.listing( - item[0] for item in sorted(results[1]))]) - - if bmc and (show_all or args['--memory'] or args['--cpu']): - results = self.get_create_options(ds_options, 'server_core') - memory_cpu_table = formatting.Table(['memory', 'cpu']) - for result in results: - memory_cpu_table.add_row([ - result[0], - formatting.listing( - [item[0] for item in sorted( - result[1], key=lambda x: int(x[0]) - )])]) - table.add_row(['memory/cpu', memory_cpu_table]) - - if args['--os'] or show_all: - results = self.get_create_options(ds_options, 'os') - - for result in results: - table.add_row([ - result[0], - formatting.listing( - [item[0] for item in sorted(result[1])], - separator=os.linesep - )]) - - if args['--disk'] or show_all: - results = self.get_create_options(ds_options, 'disk')[0] - - table.add_row([ - results[0], - formatting.listing( - [item[0] for item in sorted(results[1])], - separator=os.linesep - )]) - - if args['--nic'] or show_all: - results = self.get_create_options(ds_options, 'nic') - - for result in results: - table.add_row([result[0], formatting.listing( - item[0] for item in sorted(result[1],))]) - - if (args['--controller'] or show_all) and not bmc: - results = self.get_create_options(ds_options, 'disk_controller')[0] - - table.add_row([results[0], formatting.listing( - item[0] for item in sorted(results[1],))]) - - return table - - def get_create_options(self, ds_options, section, pretty=True): - """ This method can be used to parse the bare metal instance creation - options into different sections. This can be useful for data validation - as well as printing the options on a help screen. - - :param dict ds_options: The instance options to parse. Must come from - the .get_bare_metal_create_options() function - in the HardwareManager. - :param string section: The section to parse out. - :param bool pretty: If true, it will return the results in a 'pretty' - format that's easier to print. - """ - return_value = None - - if 'datacenter' == section: - datacenters = [loc['keyname'] - for loc in ds_options['locations']] - return_value = [('datacenter', datacenters)] - elif 'cpu' == section and 'server' in ds_options['categories']: - results = [] - - for item in ds_options['categories']['server']['items']: - results.append(( - item['description'], - item['price_id'] - )) - - return_value = results - elif 'memory' == section and 'ram' in ds_options['categories']: - ram = [] - for option in ds_options['categories']['ram']['items']: - ram.append((int(option['capacity']), option['price_id'])) - - return_value = [('memory', ram)] - elif ('server_core' == section - and 'server_core' in ds_options['categories']): - mem_options = {} - cpu_regex = re.compile(r'(\d+) x ') - memory_regex = re.compile(r' - (\d+) GB Ram', re.I) - - for item in ds_options['categories']['server_core']['items']: - cpu = cpu_regex.search(item['description']).group(1) - memory = memory_regex.search(item['description']).group(1) - - if cpu and memory: - if memory not in mem_options: - mem_options[memory] = [] - - mem_options[memory].append((cpu, item['price_id'])) - - results = [] - for memory in sorted(mem_options.keys(), key=int): - key = memory - - if pretty: - key = memory - - results.append((key, mem_options[memory])) - - return_value = results - elif 'os' == section: - os_regex = re.compile(r'(^[A-Za-z\s\/\-]+) ([\d\.]+)') - bit_regex = re.compile(r' \((\d+)\s*bit') - extra_regex = re.compile(r' - (.+)\(') - - os_list = {} - flat_list = [] - - # Loop through the operating systems and get their OS codes - for opsys in ds_options['categories']['os']['items']: - if 'Windows Server' in opsys['description']: - os_code = self._generate_windows_code(opsys['description']) - else: - os_results = os_regex.search(opsys['description']) - - # Skip this operating system if it's not parsable - if os_results is None: - continue - - name = os_results.group(1) - version = os_results.group(2) - bits = bit_regex.search(opsys['description']) - extra_info = extra_regex.search(opsys['description']) - - if bits: - bits = bits.group(1) - if extra_info: - extra_info = extra_info.group(1) - - os_code = self._generate_os_code(name, version, bits, - extra_info) - - name = os_code.split('_')[0] - - if name not in os_list: - os_list[name] = [] - - os_list[name].append((os_code, opsys['price_id'])) - flat_list.append((os_code, opsys['price_id'])) - - if pretty: - results = [] - for opsys in sorted(os_list.keys()): - results.append(('os (%s)' % opsys, os_list[opsys])) - - return_value = results - else: - return_value = [('os', flat_list)] - - elif 'disk' == section: - disks = [] - type_regex = re.compile(r'^[\d\.]+[GT]B\s+(.+)$') - for disk in ds_options['categories']['disk0']['items']: - disk_type = 'SATA' - disk_type = type_regex.match(disk['description']).group(1) - - disk_type = disk_type.replace('RPM', '').strip() - disk_type = disk_type.replace(' ', '_').upper() - disk_type = str(int(disk['capacity'])) + '_' + disk_type - disks.append((disk_type, disk['price_id'], disk['id'])) - - return_value = [('disk', disks)] - elif 'nic' == section: - single = [] - dual = [] - - for item in ds_options['categories']['port_speed']['items']: - if 'dual' in item['description'].lower(): - dual.append((str(int(item['capacity'])) + '_DUAL', - item['price_id'])) - else: - single.append((str(int(item['capacity'])), - item['price_id'])) - - return_value = [('single nic', single), ('dual nic', dual)] - elif 'disk_controller' == section: - options = [] - for item in ds_options['categories']['disk_controller']['items']: - text = item['description'].replace(' ', '') - - if 'Non-RAID' == text: - text = 'None' - - options.append((text, item['price_id'])) - - return_value = [('disk_controllers', options)] - - return return_value - - def _generate_os_code(self, name, version, bits, extra_info): - """ Encapsulates the code for generating the operating system code. """ - name = name.replace(' Linux', '') - name = name.replace('Enterprise', '') - name = name.replace('GNU/Linux', '') - - os_code = name.strip().replace(' ', '_').upper() - - if os_code.startswith('RED_HAT'): - os_code = 'REDHAT' - - if 'UBUNTU' in os_code: - version = re.sub(r'\.\d+', '', version) - - os_code += '_' + version.replace('.0', '') - - if bits: - os_code += '_' + bits - - if extra_info: - garbage = ['Install', '(32 bit)', '(64 bit)'] - - for obj in garbage: - extra_info = extra_info.replace(obj, '') - - os_code += '_' + extra_info.strip().replace(' ', '_').upper() - - return os_code - - def _generate_windows_code(self, description): - """ Separates the code for generating the Windows OS code - since it's significantly different from the rest. - """ - version_check = re.search(r'Windows Server (\d+)', description) - version = version_check.group(1) - - os_code = 'WIN_' + version - - if 'Datacenter' in description: - os_code += '-DC' - elif 'Enterprise' in description: - os_code += '-ENT' - else: - os_code += '-STD' - - if 'ith R2' in description: - os_code += '-R2' - elif 'ith Hyper-V' in description: - os_code += '-HYPERV' - - bit_check = re.search(r'\((\d+)\s*bit', description) - if bit_check: - os_code += '_' + bit_check.group(1) - - return os_code - - -class CreateServer(environment.CLIRunnable): - """ -usage: sl server create [--disk=SIZE...] [--key=KEY...] [options] - -Order/create a dedicated server. See 'sl server list-chassis' and -'sl server create-options' for valid options. - -Required: - -H --hostname=HOST Host portion of the FQDN. example: server - -D --domain=DOMAIN Domain portion of the FQDN. example: example.com - --chassis=CHASSIS The chassis to use for the new server - -c --cpu=CPU CPU model - -o OS, --os=OS OS install code. - -m --memory=MEMORY Memory in gigabytes. example: 4 - --billing=BILLING Billing rate. Options are "monthly" (default) or - "hourly". The hourly rate is only available on the - "Bare Metal Instance" chassis. - -Optional: - -d, --datacenter=DC Datacenter name - Note: Omitting this value defaults to the first - available datacenter - -n, --network=MBPS Network port speed in Mbps - --disk=SIZE... Disks. Can be specified multiple times - --controller=RAID The RAID configuration for the server. - Defaults to None. - -i, --postinstall=URI Post-install script to download - -k KEY, --key=KEY SSH keys to assign to the root user. Can be specified - multiple times. - --test Do not create the server, just get a quote - --vlan_public=VLAN The ID of the public VLAN on which you want the - hardware placed - --vlan_private=VLAN The ID of the private VLAN on which you want the - hardware placed - -t, --template=FILE A template file that defaults the command-line - options using the long name in INI format - --export=FILE Exports options to a template file -""" - action = 'create' - options = ['confirm'] - required_params = ['--hostname', '--domain', '--chassis', '--cpu', - '--memory', '--os'] - - def execute(self, args): - template.update_with_template_args(args, list_args=['--disk', '--key']) - mgr = SoftLayer.HardwareManager(self.client) - self._validate_args(args) - - ds_options = mgr.get_dedicated_server_create_options(args['--chassis']) - - order = self._process_args(args, ds_options) - - # Do not create hardware server with --test or --export - do_create = not (args['--export'] or args['--test']) - - output = None - if args.get('--test'): - result = mgr.verify_order(**order) - - table = formatting.Table(['Item', 'cost']) - table.align['Item'] = 'r' - table.align['cost'] = 'r' - - total = 0.0 - for price in result['prices']: - total += float(price.get('recurringFee', 0.0)) - rate = "%.2f" % float(price['recurringFee']) - - table.add_row([price['item']['description'], rate]) - - table.add_row(['Total monthly cost', "%.2f" % total]) - output = [] - output.append(table) - output.append(formatting.FormattedItem( - '', - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guaranteed.')) - - if args['--export']: - export_file = args.pop('--export') - template.export_to_template(export_file, args, - exclude=['--wait', '--test']) - return 'Successfully exported options to a template file.' - - if do_create: - if args['--really'] or formatting.confirm( - "This action will incur charges on your account. " - "Continue?"): - result = mgr.place_order(**order) - - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - table.add_row(['id', result['orderId']]) - table.add_row(['created', result['orderDate']]) - output = table - else: - raise exceptions.CLIAbort('Aborting dedicated server order.') - - return output - - def _process_args(self, args, ds_options): - """ - Helper method to centralize argument processing without convoluting - code flow of the main execute method. - """ - mgr = SoftLayer.HardwareManager(self.client) - - order = { - 'hostname': args['--hostname'], - 'domain': args['--domain'], - 'bare_metal': False, - 'package_id': args['--chassis'], - } - - # Determine if this is a "Bare Metal Instance" or regular server - bmc = False - if args['--chassis'] == str(mgr.get_bare_metal_package_id()): - bmc = True - - # Convert the OS code back into a price ID - os_price = self._get_price_id_from_options(ds_options, 'os', - args['--os']) - - if os_price: - order['os'] = os_price - else: - raise exceptions.CLIAbort('Invalid operating system specified.') - - order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' - - if bmc: - order['server'] = self._get_cpu_and_memory_price_ids( - ds_options, args['--cpu'], args['--memory']) - order['bare_metal'] = True - - if args['--billing'] == 'hourly': - order['hourly'] = True - else: - order['server'] = args['--cpu'] - order['ram'] = self._get_price_id_from_options( - ds_options, 'memory', int(args['--memory'])) - - # Set the disk sizes - disk_prices = [] - disk_number = 0 - for disk in args.get('--disk'): - disk_price = self._get_disk_price(ds_options, disk, disk_number) - disk_number += 1 - if disk_price: - disk_prices.append(disk_price) - - if not disk_prices: - disk_prices.append(self._get_default_value(ds_options, 'disk0')) - - order['disks'] = disk_prices - - # Set the disk controller price - if not bmc: - if args.get('--controller'): - dc_price = self._get_price_id_from_options( - ds_options, 'disk_controller', args.get('--controller')) - else: - dc_price = self._get_price_id_from_options(ds_options, - 'disk_controller', - 'None') - - order['disk_controller'] = dc_price - - # Set the port speed - port_speed = args.get('--network') or '100' - - nic_price = self._get_price_id_from_options(ds_options, 'nic', - port_speed) - - if nic_price: - order['port_speed'] = nic_price - else: - raise exceptions.CLIAbort('Invalid NIC speed specified.') - - if args.get('--postinstall'): - order['post_uri'] = args.get('--postinstall') - - # Get the SSH keys - if args.get('--key'): - keys = [] - for key in args.get('--key'): - resolver = SoftLayer.SshKeyManager(self.client).resolve_ids - key_id = helpers.resolve_id(resolver, key, 'SshKey') - keys.append(key_id) - order['ssh_keys'] = keys - - if args.get('--vlan_public'): - order['public_vlan'] = args['--vlan_public'] - - if args.get('--vlan_private'): - order['private_vlan'] = args['--vlan_private'] - - return order - - def _validate_args(self, args): - """ Raises an ArgumentError if the given arguments are not valid """ - invalid_args = [k for k in self.required_params if args.get(k) is None] - if invalid_args: - raise exceptions.ArgumentError('Missing required options: %s' - % ','.join(invalid_args)) - - def _get_default_value(self, ds_options, option): - """ Returns a 'free' price id given an option """ - if option not in ds_options['categories']: - return - - for item in ds_options['categories'][option]['items']: - if not any([ - float(item.get('setupFee', 0)), - float(item.get('recurringFee', 0)), - float(item.get('hourlyRecurringFee', 0)), - float(item.get('oneTimeFee', 0)), - float(item.get('laborFee', 0)), - ]): - return item['price_id'] - - def _get_disk_price(self, ds_options, value, number): - """ Returns a price id that matches a given disk config """ - if not number: - return self._get_price_id_from_options(ds_options, 'disk', value) - # This will get the item ID for the matching identifier string, which - # we can then use to get the price ID for our specific disk - item_id = self._get_price_id_from_options(ds_options, 'disk', - value, True) - key = 'disk' + str(number) - if key in ds_options['categories']: - for item in ds_options['categories'][key]['items']: - if item['id'] == item_id: - return item['price_id'] - - def _get_cpu_and_memory_price_ids(self, ds_options, cpu_value, - memory_value): - """ - Returns a price id for a cpu/memory pair in pre-configured servers - (formerly known as BMC). - """ - ds_obj = ServerCreateOptions() - for memory, options in ds_obj.get_create_options(ds_options, - 'server_core', - False): - if memory == memory_value: - for cpu_size, price_id in options: - if cpu_size == cpu_value: - return price_id - - def _get_price_id_from_options(self, ds_options, option, value, - item_id=False): - """ Returns a price_id for a given option and value """ - ds_obj = ServerCreateOptions() - - for _, options in ds_obj.get_create_options(ds_options, option, False): - for item_options in options: - if item_options[0] == value: - if not item_id: - return item_options[1] - return item_options[2] - - -class EditServer(environment.CLIRunnable): - """ -usage: sl server edit [options] - -Edit hardware details - -Options: - -D --domain=DOMAIN Domain portion of the FQDN example: example.com - -F --userfile=FILE Read userdata from file - -H --hostname=HOST Host portion of the FQDN. example: server - -u --userdata=DATA User defined metadata string -""" - action = 'edit' - - def execute(self, args): - data = {} - - if args['--userdata'] and args['--userfile']: - raise exceptions.ArgumentError( - '[-u | --userdata] not allowed with [-F | --userfile]') - if args['--userfile']: - if not os.path.exists(args['--userfile']): - raise exceptions.ArgumentError( - 'File does not exist [-u | --userfile] = %s' - % args['--userfile']) - - if args.get('--userdata'): - data['userdata'] = args['--userdata'] - elif args.get('--userfile'): - with open(args['--userfile'], 'r') as userfile: - data['userdata'] = userfile.read() - - data['hostname'] = args.get('--hostname') - data['domain'] = args.get('--domain') - - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - if not mgr.edit(hw_id, **data): - raise exceptions.CLIAbort("Failed to update hardware") diff --git a/SoftLayer/CLI/modules/snapshot.py b/SoftLayer/CLI/modules/snapshot.py deleted file mode 100644 index a7d95b3b5..000000000 --- a/SoftLayer/CLI/modules/snapshot.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -usage: sl snapshot [] [...] [options] - -Manage, order, delete iSCSI snapshots - -The available commands are: - cancel Cancel an iSCSI snapshot - create Create a snapshot of given iSCSI volume - create-space Orders space for storing snapshots - list List snpshots of given iSCSI - restore-volume Restores volume from existing snapshot - -For several commands will be asked for.This can be the id -of iSCSI volume or iSCSI snapshot. -""" -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - - -class CreateSnapshot(environment.CLIRunnable): - - """ -usage: sl snapshot create [options] - -Create a snapshot of the iSCSI volume. - -Examples: - sl snapshot create 123456 --note='Backup' - sl snapshot create 123456 - -Options: - --notes=NOTE An optional snapshot's note - -""" - action = 'create' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - notes = args.get('--notes') - iscsi_mgr.create_snapshot(iscsi_id, notes) - - -class CreateSnapshotSpace(environment.CLIRunnable): - - """ -usage: sl snapshot create-space [options] - -Orders snapshot space for given iSCSI. - -Examples: - sl snapshot create-space 123456 --capacity=20 - -Required : - --capacity=CAPACITY Size of snapshot space to create -""" - - action = 'create-space' - required_params = ['--capacity'] - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - invalid_args = [k for k in self.required_params if args.get(k) is None] - if invalid_args: - raise exceptions.ArgumentError('Missing required options: %s' - % ','.join(invalid_args)) - iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - capacity = args.get('--capacity') - iscsi_mgr.create_snapshot_space(iscsi_id, capacity) - - -class CancelSnapshot(environment.CLIRunnable): - - """ -usage: sl snapshot cancel [options] - -Cancel/Delete iSCSI snapshot. - -""" - action = 'cancel' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - snapshot_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'Snapshot') - iscsi_mgr.delete_snapshot(snapshot_id) - - -class RestoreVolumeFromSnapshot(environment.CLIRunnable): - - """ -usage: sl snapshot restore-volume - -restores volume from existing snapshot. - -""" - action = 'restore-volume' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - volume_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - snapshot_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'Snapshot') - iscsi_mgr.restore_from_snapshot(volume_id, snapshot_id) - - -class ListSnapshots(environment.CLIRunnable): - - """ -usage: sl snapshot list - -List iSCSI Snapshots -""" - action = 'list' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - iscsi = self.client['Network_Storage_Iscsi'] - snapshots = iscsi.getPartnerships( - mask='volumeId,partnerVolumeId,createDate,type', id=iscsi_id) - snapshots = [utils.NestedDict(n) for n in snapshots] - - table = formatting.Table([ - 'id', - 'createDate', - 'name', - 'description', - ]) - - for snapshot in snapshots: - table.add_row([ - snapshot['partnerVolumeId'], - snapshot['createDate'], - snapshot['type']['name'], - snapshot['type']['description'], - ]) - return table diff --git a/SoftLayer/CLI/modules/sshkey.py b/SoftLayer/CLI/modules/sshkey.py deleted file mode 100644 index 9649f355f..000000000 --- a/SoftLayer/CLI/modules/sshkey.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -usage: sl sshkey [] [...] [options] - -Manage SSH keys - -The available commands are: - add Add a new SSH key to your account - remove Removes an SSH key - edit Edits information about the SSH key - list Display a list of SSH keys on your account - print Prints out an SSH key -""" -# :license: MIT, see LICENSE for more details. - -from os import path - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - - -class AddSshKey(environment.CLIRunnable): - """ -usage: sl sshkey add