From 011b7fc45a99543a994c8a6d2c2248d984d2484a Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 18 Feb 2015 15:33:07 -0600 Subject: [PATCH 01/57] basics for customizable column list --- SoftLayer/CLI/virt/list.py | 44 ++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 9fdf8bf55..896d19674 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -28,13 +28,16 @@ @click.option('--tags', help='Show instances that have one of these comma-separated ' 'tags') +@click.option('--column', help='Columns to display. default is ' + ' guid, hostname, primary_ip, backend_ip, datacenter, action', + default="guid,hostname,primary_ip,backend_ip,datacenter,action") @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tags): + hourly, monthly, tags, column): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) - + columns = [col.strip() for col in column.split(',')] tag_list = None if tags: tag_list = [tag.strip() for tag in tags.split(',')] @@ -49,25 +52,28 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, nic_speed=network, tags=tag_list) - table = formatting.Table([ - 'guid', - 'hostname', - 'primary_ip', - 'backend_ip', - 'datacenter', - 'action', - ]) + table = formatting.Table(columns) table.sortby = sortby or 'hostname' - for guest in guests: guest = utils.NestedDict(guest) - table.add_row([ - guest['globalIdentifier'] or guest['id'], - guest['hostname'], - guest['primaryIpAddress'] or formatting.blank(), - guest['primaryBackendIpAddress'] or formatting.blank(), - guest['datacenter']['name'] or formatting.blank(), - formatting.active_txn(guest), - ]) + row_column = [] + for col in columns: + entry = None + if col == 'guid': + entry = guest['globalIdentifier'] + elif col == 'datacenter': + entry = guest['datacenter']['name'] + elif col == 'primary_ip': + entry = guest['primaryIpAddress'] + elif col == 'backend_ip': + entry = guest['primaryBackendIpAddress'] + elif col == 'action': + entry = formatting.active_txn(guest) + else: + entry = guest[col] + + row_column.append(entry or formatting.blank()) + + table.add_row(row_column) return table From 1b9f8c77adf2d78e22e0d436cbbcffe04d61cbbb Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Tue, 10 Mar 2015 17:58:36 -0500 Subject: [PATCH 02/57] added column list to hw list --- SoftLayer/CLI/server/list.py | 56 +++++++++++---------- SoftLayer/CLI/virt/list.py | 32 ++++++------ SoftLayer/tests/CLI/modules/server_tests.py | 36 ++++++------- SoftLayer/utils.py | 4 +- 4 files changed, 63 insertions(+), 65 deletions(-) diff --git a/SoftLayer/CLI/server/list.py b/SoftLayer/CLI/server/list.py index e45b00311..e165a89c4 100644 --- a/SoftLayer/CLI/server/list.py +++ b/SoftLayer/CLI/server/list.py @@ -12,25 +12,25 @@ @click.command() -@click.option('--sortby', - help='Column to sort by', - type=click.Choice(['guid', - 'hostname', - 'primary_ip', - 'backend_ip', - 'datacenter'])) +@click.option('--sortby', help='Column to sort by', + default='hostname') @click.option('--cpu', '-c', help='Filter by number of CPU cores') @click.option('--domain', '-D', help='Filter by domain') @click.option('--datacenter', '-d', help='Filter by datacenter') @click.option('--hostname', '-H', help='Filter by hostname') @click.option('--memory', '-m', help='Filter by memory in gigabytes') @click.option('--network', '-n', help='Filter by network port speed in Mbps') +@click.option('--column', help='Columns to display. default is ' + ' guid, hostname, primary_ip, backend_ip, datacenter, action', + default="guid,hostname,primary_ip,backend_ip,datacenter,action") @helpers.multi_option('--tag', help='Filter by tags') @environment.pass_env -def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag): +def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, + column): """List hardware servers.""" manager = SoftLayer.HardwareManager(env.client) + columns = [col.strip() for col in column.split(',')] servers = manager.list_hardware(hostname=hostname, domain=domain, @@ -40,27 +40,29 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag): nic_speed=network, tags=tag) - table = formatting.Table([ - 'guid', - 'hostname', - 'primary_ip', - 'backend_ip', - 'datacenter', - 'action', - ]) - table.sortby = sortby or 'hostname' + table = formatting.Table(columns) + table.sortby = sortby + column_map = {} + column_map['guid'] = 'globalIdentifier' + column_map['primary_ip'] = 'primaryIpAddress' + column_map['backend_ip'] = 'primaryBackendIpAddress' + column_map['datacenter'] = 'datacenter-name' + column_map['action'] = 'formatted-action' for server in servers: server = utils.NestedDict(server) - # NOTE(kmcdonald): There are cases where a server might not have a - # globalIdentifier. - table.add_row([ - server['globalIdentifier'] or server['id'], - server['hostname'], - server['primaryIpAddress'] or formatting.blank(), - server['primaryBackendIpAddress'] or formatting.blank(), - server['datacenter']['name'] or formatting.blank(), - formatting.active_txn(server), - ]) + server['datacenter-name'] = server['datacenter']['name'] + server['formatted-action'] = formatting.active_txn(server) + row_column = [] + for col in columns: + entry = None + if col in column_map: + entry = server[column_map[col]] + else: + entry = server[col] + + row_column.append(entry or formatting.blank()) + + table.add_row(row_column) return table diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 896d19674..342ff9c5c 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -10,13 +10,8 @@ @click.command() -@click.option('--sortby', - help='Column to sort by', - type=click.Choice(['guid', - 'hostname', - 'primary_ip', - 'backend_ip', - 'datacenter'])) +@click.option('--sortby', help='Column to sort by', + default='hostname') @click.option('--cpu', '-c', help='Number of CPU cores', type=click.INT) @click.option('--domain', '-D', help='Domain portion of the FQDN') @click.option('--datacenter', '-d', help='Datacenter shortname') @@ -53,22 +48,23 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tags=tag_list) table = formatting.Table(columns) - table.sortby = sortby or 'hostname' + table.sortby = sortby + column_map = {} + column_map['guid'] = 'globalIdentifier' + column_map['primary_ip'] = 'primaryIpAddress' + column_map['backend_ip'] = 'primaryBackendIpAddress' + column_map['datacenter'] = 'datacenter-name' + column_map['action'] = 'formatted-action' + for guest in guests: guest = utils.NestedDict(guest) + guest['datacenter-name'] = guest['datacenter']['name'] + guest['formatted-action'] = formatting.active_txn(guest) row_column = [] for col in columns: entry = None - if col == 'guid': - entry = guest['globalIdentifier'] - elif col == 'datacenter': - entry = guest['datacenter']['name'] - elif col == 'primary_ip': - entry = guest['primaryIpAddress'] - elif col == 'backend_ip': - entry = guest['primaryBackendIpAddress'] - elif col == 'action': - entry = formatting.active_txn(guest) + if col in column_map: + entry = guest[column_map[col]] else: entry = guest[col] diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index 675fae24b..cb450a440 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -136,28 +136,28 @@ def test_list_servers(self): expected = [ { - 'datacenter': 'TEST00', - 'primary_ip': '172.16.1.100', - 'hostname': 'hardware-test1', - 'guid': '1a2b3c-1701', - 'backend_ip': '10.1.0.2', - 'action': 'TXN_NAME', + u'datacenter': u'TEST00', + u'primary_ip': u'172.16.1.100', + u'hostname': u'hardware-test1', + u'guid': u'1a2b3c-1701', + u'backend_ip': u'10.1.0.2', + u'action': u'TXN_NAME', }, { - 'datacenter': 'TEST00', - 'primary_ip': '172.16.4.94', - 'hostname': 'hardware-test2', - 'guid': '1a2b3c-1702', - 'backend_ip': '10.1.0.3', - 'action': None, + u'datacenter': u'TEST00', + u'primary_ip': u'172.16.4.94', + u'hostname': u'hardware-test2', + u'guid': u'1a2b3c-1702', + u'backend_ip': u'10.1.0.3', + u'action': None, }, { - 'datacenter': 'TEST00', - 'primary_ip': '172.16.4.95', - 'hostname': 'hardware-bad-memory', - 'guid': 1002, - 'backend_ip': '10.1.0.4', - 'action': None, + u'datacenter': u'TEST00', + u'primary_ip': u'172.16.4.95', + u'hostname': u'hardware-bad-memory', + u'guid': None, + u'backend_ip': u'10.1.0.4', + u'action': None, } ] diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 622710f71..9ef59e583 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -1,7 +1,7 @@ """ SoftLayer.utils ~~~~~~~~~~~~~~~ - Utility function/classes + Utility function/classes. :license: MIT, see LICENSE for more details. """ @@ -180,7 +180,7 @@ def resolve_ids(identifier, resolvers): class UTC(datetime.tzinfo): - """UTC timezone""" + """UTC timezone.""" def utcoffset(self, _): return datetime.timedelta(0) From a328f95ff8787267c5cd16692a694c11e7e355b6 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Tue, 10 Mar 2015 18:22:47 -0500 Subject: [PATCH 03/57] added nice error handler for badly sorted tables --- SoftLayer/CLI/formatting.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index 1f7e495a6..bb41b9430 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -13,6 +13,7 @@ import click import prettytable +from SoftLayer.CLI import exceptions from SoftLayer import utils FALSE_VALUES = ['0', 'false', 'FALSE', 'no', 'False'] @@ -274,7 +275,11 @@ def prettytable(self): """Returns a new prettytable instance.""" table = prettytable.PrettyTable(self.columns) if self.sortby: - table.sortby = self.sortby + if self.sortby in self.columns: + table.sortby = self.sortby + else: + msg = "Column (%s) doesn't exist to sort by" % self.sortby + raise exceptions.CLIAbort(msg) for a_col, alignment in self.align.items(): table.align[a_col] = alignment From 3c501173d5b14086f547e675fe86d8e4d4065963 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Tue, 10 Mar 2015 18:30:47 -0500 Subject: [PATCH 04/57] undid an accidental test change --- SoftLayer/tests/CLI/modules/server_tests.py | 36 ++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index cb450a440..28bf9d1db 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -136,28 +136,28 @@ def test_list_servers(self): expected = [ { - u'datacenter': u'TEST00', - u'primary_ip': u'172.16.1.100', - u'hostname': u'hardware-test1', - u'guid': u'1a2b3c-1701', - u'backend_ip': u'10.1.0.2', - u'action': u'TXN_NAME', + 'datacenter': 'TEST00', + 'primary_ip': '172.16.1.100', + 'hostname': 'hardware-test1', + 'guid': '1a2b3c-1701', + 'backend_ip': '10.1.0.2', + 'action': 'TXN_NAME', }, { - u'datacenter': u'TEST00', - u'primary_ip': u'172.16.4.94', - u'hostname': u'hardware-test2', - u'guid': u'1a2b3c-1702', - u'backend_ip': u'10.1.0.3', - u'action': None, + 'datacenter': 'TEST00', + 'primary_ip': '172.16.4.94', + 'hostname': 'hardware-test2', + 'guid': '1a2b3c-1702', + 'backend_ip': '10.1.0.3', + 'action': None, }, { - u'datacenter': u'TEST00', - u'primary_ip': u'172.16.4.95', - u'hostname': u'hardware-bad-memory', - u'guid': None, - u'backend_ip': u'10.1.0.4', - u'action': None, + 'datacenter': 'TEST00', + 'primary_ip': '172.16.4.95', + 'hostname': 'hardware-bad-memory', + 'guid': None, + 'backend_ip': '10.1.0.4', + 'action': None, } ] From 14b7f4dc68193dbfbc3c671d29781888a74639b1 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 11 Mar 2015 16:58:06 -0500 Subject: [PATCH 05/57] Adding some examples for the vs manager --- SoftLayer/managers/vs.py | 197 ++++++++++++++++++++++++++++++--------- 1 file changed, 154 insertions(+), 43 deletions(-) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 64ae95770..6cfd0257e 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -23,6 +23,17 @@ class VSManager(utils.IdentifierMixin, object): manager to handle ordering. If none is provided, one will be auto initialized. + + Example:: + # Initialize the VSManager. + # env variables. These can also be specified in ~/.softlayer, + # or passed directly to SoftLayer.Client() + # SL_USERNAME = YOUR_USERNAME + # SL_API_KEY = YOUR_API_KEY + import SoftLayer + client = SoftLayer.Client() + mgr = SoftLayer.VSManager(client) + """ def __init__(self, client, ordering_manager=None): @@ -57,18 +68,17 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, :returns: Returns a list of dictionaries representing the matching virtual servers - :: + Example:: - # Print out a list of all hourly instances in the DAL05 data center. - # env variables - # SL_USERNAME = YOUR_USERNAME - # SL_API_KEY = YOUR_API_KEY - import SoftLayer - client = SoftLayer.Client() + # Print out a list of all hourly instances in the DAL05 data center. - mgr = SoftLayer.VSManager(client) - for vsi in mgr.list_instances(hourly=True, datacenter='dal05'): - print vsi['fullyQualifiedDomainName'], vs['primaryIpAddress'] + for vsi in mgr.list_instances(hourly=True, datacenter='dal05'): + print vsi['fullyQualifiedDomainName'], vsi['primaryIpAddress'] + + # Using a custom object-mask. Will get ONLY what is specified + object_mask = "mask[hostname,monitoringRobot[robotStatus]]" + for vsi in mgr.list_instances(mask=object_mask,hourly=True): + print vsi """ if 'mask' not in kwargs: @@ -147,18 +157,16 @@ def get_instance(self, instance_id, **kwargs): :returns: A dictionary containing a large amount of information about the specified instance. - :: + Example:: - # Print out the FQDN and IP address for instance ID 12345. - # env variables - # SL_USERNAME = YOUR_USERNAME - # SL_API_KEY = YOUR_API_KEY - import SoftLayer - client = SoftLayer.Client() + # Print out instance ID 12345. + vsi = mgr.get_instance(12345) + print vsi - mgr = SoftLayer.VSManager(client) - vsi = mgr.get_instance(12345) - print vsi['fullyQualifiedDomainName'], vs['primaryIpAddress'] + # Print out only FQDN and primaryIP for instance 12345 + object_mask = "mask[fullyQualifiedDomainName,primaryIpAddress]" + vsi = mgr.get_instance(12345, mask=mask) + print vsi """ @@ -211,6 +219,11 @@ def get_create_options(self): :returns: A dictionary of creation options. + Example:: + + # Prints out the create option dictionary + options = mgr.get_create_options() + print(options) """ return self.guest.getCreateObjectOptions() @@ -219,17 +232,10 @@ def cancel_instance(self, instance_id): :param integer instance_id: the instance ID to cancel - :: + Example:: - # Cancel for instance ID 12345. - # env variables - # SL_USERNAME = YOUR_USERNAME - # SL_API_KEY = YOUR_API_KEY - import SoftLayer - client = SoftLayer.Client() - - mgr = SoftLayer.VSManager(client) - mgr.cancel_instance(12345) + # Cancels instance 12345 + mgr.cancel_instance(12345) """ return self.guest.deleteObject(id=instance_id) @@ -242,17 +248,15 @@ def reload_instance(self, instance_id, post_uri=None, ssh_keys=None): after reload :param list ssh_keys: The SSH keys to add to the root user - :: + .. warning:: + Post-provision script MUST be HTTPS for it to be executed. + This will reformat the primary drive. - # Reload instance ID 12345 then run a custom post-provision script. - # env variables - # SL_USERNAME = YOUR_USERNAME - # SL_API_KEY = YOUR_API_KEY - import SoftLayer - client = SoftLayer.Client() + Example:: + # Reload instance ID 12345 then run a custom post-provision script. + # Post-provision script MUST be HTTPS for it to be executed. post_uri = 'https://somehost.com/bootstrap.sh' - mgr = SoftLayer.VSManager(client) vsi = mgr.reload_instance(12345, post_uri=post_url) """ @@ -384,6 +388,10 @@ def wait_for_ready(self, instance_id, limit, delay=1, pending=False): Defaults to 1. :param bool pending: Wait for pending transactions not related to provisioning or reloads such as monitoring. + + Example:: + # Will return once vsi 12345 is ready, or after 10 checks + ready = mgr.wait_for_ready(12345, 10) """ for count, new_instance in enumerate(itertools.repeat(instance_id), start=1): @@ -423,6 +431,27 @@ def verify_create_instance(self, **kwargs): Without actually placing an order. See :func:`create_instance` for a list of available options. + + Example:: + new_vsi = { + 'domain': u'test01.labs.sftlyr.ws', + 'hostname': u'minion05', + 'datacenter': u'hkg02', + 'dedicated': False, + 'private': False, + 'cpus': 1, + 'os_code' : u'UBUNTU_LATEST', + 'hourly': True, + 'ssh_keys': [1234], + 'disks': ('100','25'), + 'local_disk': True, + 'memory': 1024 + } + + vsi = mgr.verify_create_instance(**new_vsi) + # vsi will be a SoftLayer_Container_Product_Order_Virtual_Guest + # if your order is correct. Otherwise you will get an exception + print vsi """ create_options = self._generate_create_dict(**kwargs) return self.guest.generateOrderTemplate(create_options) @@ -459,6 +488,30 @@ def create_instance(self, **kwargs): :param list ssh_keys: The SSH keys to add to the root user :param int nic_speed: The port speed to set :param string tags: tags to set on the VS as a comma separated list + + .. warning:: + This will add charges to your account + + Example:: + new_vsi = { + 'domain': u'test01.labs.sftlyr.ws', + 'hostname': u'minion05', + 'datacenter': u'hkg02', + 'dedicated': False, + 'private': False, + 'cpus': 1, + 'os_code' : u'UBUNTU_LATEST', + 'hourly': True, + 'ssh_keys': [1234], + 'disks': ('100','25'), + 'local_disk': True, + 'memory': 1024, + 'tags': 'test, pleaseCancel' + } + + vsi = mgr.create_instance(**new_vsi) + # vsi will have the newly created vsi details if done properly. + print vsi """ tags = kwargs.pop('tags', None) inst = self.guest.createObject(self._generate_create_dict(**kwargs)) @@ -471,6 +524,39 @@ def create_instances(self, config_list): This takes a list of dictionaries using the same arguments as create_instance(). + + .. warning:: + This will add charges to your account + + Example:: + # Define the instance we want to create. + new_vsi = { + 'domain': u'test01.labs.sftlyr.ws', + 'hostname': u'multi-test', + 'datacenter': u'hkg02', + 'dedicated': False, + 'private': False, + 'cpus': 1, + 'os_code' : u'UBUNTU_LATEST', + 'hourly': True, + 'ssh_keys': [87634], + 'disks': ('100','25'), + 'local_disk': True, + 'memory': 1024, + 'tags': 'test, pleaseCancel' + } + + # using .copy() so we can make changes to individual nodes + instances = [new_vsi.copy(), new_vsi.copy(), new_vsi.copy()] + + # give each its own hostname, not required. + instances[0]['hostname'] = "multi-test01" + instances[1]['hostname'] = "multi-test02" + instances[2]['hostname'] = "multi-test03" + + vsi = mgr.create_instances(config_list=instances) + #vsi will be a dictionary of all the new virtual servers + print vsi """ tags = [conf.pop('tags', None) for conf in config_list] @@ -491,6 +577,16 @@ def change_port_speed(self, instance_id, public, speed): True (default) means the public interface. False indicates the private interface. :param int speed: The port speed to set. + + .. warning:: + A port speed of 0 will disable the interface. + + Example:: + #change the Public interface to 10Mbps on instance 12345 + result = mgr.change_port_speed(instance_id=12345, + public=True, speed=10) + # result will be True or an Exception + print result """ if public: func = self.guest.setPublicNetworkInterfaceSpeed @@ -535,7 +631,13 @@ def edit(self, instance_id, userdata=None, hostname=None, domain=None, :param string notes: notes about this particular VS :param string tags: tags to set on the VS as a comma separated list. Use the empty string to remove all tags. + :returns: bool -- True or an Exception + Example:: + # Change the hostname on instance 12345 to 'something' + result = mgr.edit(instance_id=12345 , hostname="something") + #result will be True or an Exception + print vsi """ obj = {} @@ -563,6 +665,11 @@ def rescue(self, instance_id): """Reboot a VSI into the Xen recsue kernel. :param integer instance_id: the instance ID to rescue + :returns: bool -- True or an Exception + + Example:: + # Puts instance 12345 into rescue mode + result = mgr.rescue(instance_id=12345) """ return self.guest.executeRescueLayer(id=instance_id) @@ -577,6 +684,13 @@ def capture(self, instance_id, name, additional_disks=False, notes=None): attached storage devices :param string notes: notes about this particular image + :returns: dictionary -- information about the capture transaction. + + Example:: + name = "Testing Images" + notes = "Some notes about this image" + result = mgr.capture(instance_id=12345, name=name, notes=notes) + print result """ vsi = self.get_instance(instance_id) @@ -602,11 +716,8 @@ def upgrade(self, instance_id, cpus=None, memory=None, :param int memory: RAM of the VS to be upgraded to. :param int nic_speed: The port speed to set - :: - - # Upgrade instance 12345 to 4 CPUs and 4 GB of memory - import SoftLayer - client = SoftLayer.Client(config="~/.softlayer") + :returns: bool + Example:: mgr = SoftLayer.VSManager(client) mgr.upgrade(12345, cpus=4, memory=4) From e2fbcd9dd2307d424c8e2ee5b46941b49350d888 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 13 Mar 2015 19:53:57 -0500 Subject: [PATCH 06/57] Added examples to hardware, ssl --- SoftLayer/managers/hardware.py | 119 ++++++++++++++++++++++++++++++--- SoftLayer/managers/ssl.py | 40 +++++++++++ SoftLayer/managers/vs.py | 2 +- 3 files changed, 150 insertions(+), 11 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 434890e22..c2178a303 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -21,6 +21,16 @@ class HardwareManager(utils.IdentifierMixin, object): manager to handle ordering. If none is provided, one will be auto initialized. + Example:: + # Initialize the Manager. + # env variables. These can also be specified in ~/.softlayer, + # or passed directly to SoftLayer.Client() + # SL_USERNAME = YOUR_USERNAME + # SL_API_KEY = YOUR_API_KEY + import SoftLayer + client = SoftLayer.Client() + mgr = SoftLayer.HardwareManager(client) + """ def __init__(self, client, ordering_manager=None): self.client = client @@ -41,6 +51,14 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', come from :func:`get_cancellation_reasons`. :param string comment: An optional comment to include with the cancellation. + :param immediate bool: False = Cancel at end of billing cycle. + True = Cancel now, only for bareMetalInstances + + Example:: + + # Cancels hardware id 1234 + result = mrg.cancel_hardware(hardware_id=1234) + print result """ # Check to see if this is actually a pre-configured server (BMC). They # require a different cancellation call. @@ -75,6 +93,11 @@ def cancel_metal(self, hardware_id, immediate=False): :param bool immediate: If true, the bare metal instance will be cancelled immediately. Otherwise, it will be scheduled to cancel on the anniversary date. + + Example:: + + result = mgr.cancel_metal(hardware_id=1234) + print result """ hw_billing = self.get_hardware(hardware_id, mask='mask[id, billingItem.id]') @@ -107,6 +130,14 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, hardware. This list will contain both dedicated servers and bare metal computing instances + Example:: + + # Using a custom object-mask. Will get ONLY what is specified + # These will stem from the SoftLayer_Hardware_Server datatype + object_mask = "mask[hostname,monitoringRobot[robotStatus]]" + result = mgr.list_hardware(mask=object_mask) + print result + """ if 'mask' not in kwargs: hw_items = [ @@ -183,6 +214,11 @@ def get_bare_metal_create_options(self): function will make those calls and reformat the results into a dictionary that's easier to manage. It's recommended that you cache these results with a reasonable lifetime for performance reasons. + + Example:: + + result = mgr.get_bare_metal_create_options() + print result """ hw_id = self.get_bare_metal_package_id() @@ -192,7 +228,15 @@ def get_bare_metal_create_options(self): return self._parse_package_data(hw_id) def get_bare_metal_package_id(self): - """Return the bare metal package id.""" + """Return the bare metal package id. + + :resturns: the id of the BARE_METAL_CORE package + + Example:: + + result = mgr.get_bare_metal_package_id + + """ ordering_manager = self.ordering_manager mask = "mask[id,name,description,type[keyName]]" package = ordering_manager.get_package_by_type('BARE_METAL_CORE', mask) @@ -204,6 +248,11 @@ def get_available_dedicated_server_packages(self): :returns: A list of tuples of available dedicated server packages in the form (id, name, description) + + Example:: + + result = mgr.get_available_dedicated_server_packages() + print result """ available_packages = [] ordering_manager = self.ordering_manager @@ -246,6 +295,14 @@ def get_dedicated_server_create_options(self, package_id): make those calls and reformat the results into a dictionary that's easier to manage. It's recommended that you cache these results with a reasonable lifetime for performance reasons. + + Example:: + + package_ids = mgr.get_available_dedicated_server_packages() + #pick which package you want the create options for + package_id = ?? + result = mgr.get_dedicated_server_create_options(package_id) + print result """ return self._parse_package_data(package_id) @@ -256,6 +313,12 @@ def get_hardware(self, hardware_id, **kwargs): :returns: A dictionary containing a large amount of information about the specified server. + Example:: + + object_mask = "mask[id,networkVlans[vlanNumber]]" + # Object masks are optional + result = mrg.get_hardware(hardware_id=1234,mask=object_mask) + print result """ if 'mask' not in kwargs: @@ -323,6 +386,11 @@ def rescue(self, hardware_id): """Reboot a server into the a recsue kernel. :param integer instance_id: the server ID to rescue + + Example:: + + result = mgr.rescue(1234) + print result """ return self.hardware.bootToRescueLayer(id=hardware_id) @@ -334,6 +402,16 @@ def change_port_speed(self, hardware_id, public, speed): True (default) means the public interface. False indicates the private interface. :param int speed: The port speed to set. + + .. warning:: + A port speed of 0 will disable the interface. + + Example:: + #change the Public interface to 10Mbps on instance 12345 + result = mgr.change_port_speed(hardware_id=12345, + public=True, speed=10) + # result will be True or an Exception + print result """ if public: func = self.hardware.setPublicNetworkInterfaceSpeed @@ -390,7 +468,7 @@ def place_order(self, **kwargs): following sample for an example of using HardwareManager functions for ordering a basic server. - :: + Example:: # client is assumed to be an initialized SoftLayer.API.Client object mgr = HardwareManager(client) @@ -449,8 +527,17 @@ def place_order(self, **kwargs): def verify_order(self, **kwargs): """Verifies an order for a piece of hardware. + Behaves exactly like place_order, except it doesnt actually + add a server to your account. Will let you know if the ordering + system likes your order or not. See :func:`place_order` for a list of available options. + + Example:: + + # Use the place_order example for the content of args + result = mgr.verify_order(**args) + print result """ create_options = self._generate_create_dict(**kwargs) return self.client['Product_Order'].verifyOrder(create_options) @@ -460,6 +547,11 @@ def get_cancellation_reasons(self): These can be used when cancelling a dedicated server via :func:`cancel_hardware`. + + Example:: + + result = mgr.get_cancellation_reasons() + print result """ return { 'unneeded': 'No longer needed', @@ -726,6 +818,11 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, :param string domain: valid domain name :param string notes: notes about this particular hardware + Example:: + # Change the hostname on instance 12345 to 'something' + result = mgr.edit(hardware_id=12345 , hostname="something") + #result will be True or an Exception + print result """ obj = {} @@ -746,12 +843,8 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, return self.hardware.editObject(obj, id=hardware_id) - def update_firmware(self, - hardware_id, - ipmi=True, - raid_controller=True, - bios=True, - hard_drive=True): + def update_firmware(self, hardware_id, ipmi=True, raid_controller=True, + bios=True, hard_drive=True): """Update hardware firmware. This will cause the server to be unavailable for ~20 minutes. @@ -762,6 +855,12 @@ def update_firmware(self, :param bool raid_controller: Update the raid controller firmware. :param bool bios: Update the bios firmware. :param bool hard_drive: Update the hard drive firmware. + + Example:: + + # Check the servers active transactions to see progress + result = mgr.update_firmware(hardware_id=1234) + print result """ return self.hardware.createFirmwareUpdateTransaction( @@ -780,9 +879,9 @@ def get_default_value(package_options, category, hourly=False): If the category has multiple items with no fee, this will return the first it finds and then short circuit. This may not match the default value presented on the SoftLayer ordering portal. Additionally, this - method will return None if there are no free items in the category. + method will return None if there are no fee items in the category. - :returns: Returns the price ID of the first free item it finds or None + :returns: Returns the price ID of the first fee item it finds or None if there are no free items. """ if category not in package_options['categories']: diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index 68e78dbdb..3cc13d289 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -11,6 +11,17 @@ class SSLManager(object): """Manages SSL certificates. :param SoftLayer.API.Client client: an API client instance + + Example:: + # Initialize the Manager. + # env variables. These can also be specified in ~/.softlayer, + # or passed directly to SoftLayer.Client() + # SL_USERNAME = YOUR_USERNAME + # SL_API_KEY = YOUR_API_KEY + import SoftLayer + client = SoftLayer.Client() + mgr = SoftLayer.SSLManager(client) + """ def __init__(self, client): @@ -24,6 +35,12 @@ def list_certs(self, method='all'): 'all', 'expired', and 'valid'. :returns: A list of dictionaries representing the requested SSL certs. + Example:: + + # Get all valid SSL certs + certs = mgr.list_certs(method='valid') + print certs + """ ssl = self.client['Account'] methods = { @@ -42,6 +59,11 @@ def add_certificate(self, certificate): :param dict certificate: A dictionary representing the parts of the certificate. See SLDN for more information. + Example:: + + cert = ?? + result = mgr.add_certificate(certificate=cert) + """ return self.ssl.createObject(certificate) @@ -50,6 +72,12 @@ def remove_certificate(self, cert_id): :param integer cert_id: a certificate ID to remove + Example:: + + # Removes certificate with id 1234 + result = mgr.remove_certificate(cert_id = 1234) + print result + """ return self.ssl.deleteObject(id=cert_id) @@ -61,6 +89,13 @@ def edit_certificate(self, certificate): :param dict certificate: the certificate to update. + Example:: + + # Updates the cert id 1234 + cert['id'] = 1234 + cert['certificate'] = ?? + result = mgr.edit_certificate(certificate=cert) + """ return self.ssl.editObject(certificate, id=certificate['id']) @@ -69,5 +104,10 @@ def get_certificate(self, cert_id): :param integer cert_id: the certificate ID to retrieve + Example:: + + cert = mgr.get_certificate(cert_id=1234) + print(cert) + """ return self.ssl.getObject(id=cert_id) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 6cfd0257e..4c22be783 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -637,7 +637,7 @@ def edit(self, instance_id, userdata=None, hostname=None, domain=None, # Change the hostname on instance 12345 to 'something' result = mgr.edit(instance_id=12345 , hostname="something") #result will be True or an Exception - print vsi + print result """ obj = {} From 83ea661caae7fa3ce70f9129768dca4004200ec5 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 15 Apr 2015 13:44:21 -0500 Subject: [PATCH 07/57] changed column to columns on vs and hw list --- SoftLayer/CLI/server/list.py | 10 +++++----- SoftLayer/CLI/virt/list.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/server/list.py b/SoftLayer/CLI/server/list.py index e165a89c4..d47e8ce7f 100644 --- a/SoftLayer/CLI/server/list.py +++ b/SoftLayer/CLI/server/list.py @@ -20,17 +20,17 @@ @click.option('--hostname', '-H', help='Filter by hostname') @click.option('--memory', '-m', help='Filter by memory in gigabytes') @click.option('--network', '-n', help='Filter by network port speed in Mbps') -@click.option('--column', help='Columns to display. default is ' +@click.option('--columns', help='Columns to display. default is ' ' guid, hostname, primary_ip, backend_ip, datacenter, action', default="guid,hostname,primary_ip,backend_ip,datacenter,action") @helpers.multi_option('--tag', help='Filter by tags') @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, - column): + columns): """List hardware servers.""" manager = SoftLayer.HardwareManager(env.client) - columns = [col.strip() for col in column.split(',')] + columns_clean = [col.strip() for col in columns.split(',')] servers = manager.list_hardware(hostname=hostname, domain=domain, @@ -40,7 +40,7 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, nic_speed=network, tags=tag) - table = formatting.Table(columns) + table = formatting.Table(columns_clean) table.sortby = sortby column_map = {} column_map['guid'] = 'globalIdentifier' @@ -54,7 +54,7 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, server['datacenter-name'] = server['datacenter']['name'] server['formatted-action'] = formatting.active_txn(server) row_column = [] - for col in columns: + for col in columns_clean: entry = None if col in column_map: entry = server[column_map[col]] diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 342ff9c5c..24f587510 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -23,16 +23,16 @@ @click.option('--tags', help='Show instances that have one of these comma-separated ' 'tags') -@click.option('--column', help='Columns to display. default is ' +@click.option('--columns', help='Columns to display. default is ' ' guid, hostname, primary_ip, backend_ip, datacenter, action', default="guid,hostname,primary_ip,backend_ip,datacenter,action") @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tags, column): + hourly, monthly, tags, columns): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) - columns = [col.strip() for col in column.split(',')] + columns_clean = [col.strip() for col in columns.split(',')] tag_list = None if tags: tag_list = [tag.strip() for tag in tags.split(',')] @@ -47,7 +47,7 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, nic_speed=network, tags=tag_list) - table = formatting.Table(columns) + table = formatting.Table(columns_clean) table.sortby = sortby column_map = {} column_map['guid'] = 'globalIdentifier' @@ -61,7 +61,7 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, guest['datacenter-name'] = guest['datacenter']['name'] guest['formatted-action'] = formatting.active_txn(guest) row_column = [] - for col in columns: + for col in columns_clean: entry = None if col in column_map: entry = guest[column_map[col]] From 7b36f03a04a1fdc4d084c661a530439131aed9fd Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 15 Apr 2015 15:50:04 -0500 Subject: [PATCH 08/57] minor changes --- SoftLayer/managers/hardware.py | 75 ++++------------------------------ SoftLayer/managers/ssl.py | 2 +- SoftLayer/managers/vs.py | 6 +-- 3 files changed, 12 insertions(+), 71 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 9f2103c49..2878e8add 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -59,7 +59,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', # Cancels hardware id 1234 result = mrg.cancel_hardware(hardware_id=1234) - print result + """ # Check to see if this is actually a pre-configured server (BMC). They # require a different cancellation call. @@ -97,7 +97,7 @@ def cancel_metal(self, hardware_id, immediate=False): Example:: result = mgr.cancel_metal(hardware_id=1234) - print result + """ hw_billing = self.get_hardware(hardware_id, mask='mask[id, billingItem.id]') @@ -136,7 +136,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, # These will stem from the SoftLayer_Hardware_Server datatype object_mask = "mask[hostname,monitoringRobot[robotStatus]]" result = mgr.list_hardware(mask=object_mask) - print result + """ if 'mask' not in kwargs: @@ -212,7 +212,7 @@ def get_hardware(self, hardware_id, **kwargs): object_mask = "mask[id,networkVlans[vlanNumber]]" # Object masks are optional result = mrg.get_hardware(hardware_id=1234,mask=object_mask) - print result + """ if 'mask' not in kwargs: @@ -284,7 +284,7 @@ def rescue(self, hardware_id): Example:: result = mgr.rescue(1234) - print result + """ return self.hardware.bootToRescueLayer(id=hardware_id) @@ -305,7 +305,7 @@ def change_port_speed(self, hardware_id, public, speed): result = mgr.change_port_speed(hardware_id=12345, public=True, speed=10) # result will be True or an Exception - print result + """ if public: func = self.hardware.setPublicNetworkInterfaceSpeed @@ -335,65 +335,6 @@ def place_order(self, **kwargs): :param boolean no_public: True if this server should only have private interfaces :param list extras: List of extra feature names - - .. warning:: - Due to how the ordering structure currently works, all ordering - takes place using price IDs rather than quantities. See the - following sample for an example of using HardwareManager functions - for ordering a basic server. - - Example:: - - # client is assumed to be an initialized SoftLayer.API.Client object - mgr = HardwareManager(client) - - # Package ID 32 corresponds to the 'Quad Processor, Quad Core Intel' - # package. This information can be obtained from the - # :func:`get_available_dedicated_server_packages` function. - options = mgr.get_dedicated_server_create_options(32) - - # Review the contents of options to find the information that - # applies to your order. For the sake of this example, we assume - # that your selections are a series of item IDs for each category - # organized into a key-value dictionary. - - # This contains selections for all required categories - selections = { - 'server': 542, # Quad Processor Quad Core Intel 7310 - 1.60GHz - 'pri_ip_addresses': 15, # 1 IP Address - 'notification': 51, # Email and Ticket - 'ram': 280, # 16 GB FB-DIMM Registered 533/667 - 'bandwidth': 173, # 5000 GB Bandwidth - 'lockbox': 45, # 1 GB Lockbox - 'monitoring': 49, # Host Ping - 'disk0': 14, # 500GB SATA II (for the first disk) - 'response': 52, # Automated Notification - 'port_speed': 187, # 100 Mbps Public & Private Networks - 'power_supply': 469, # Redundant Power Supplies - 'disk_controller': 487, # Non-RAID - 'vulnerability_scanner': 307, # Nessus - 'vpn_management': 309, # Unlimited SSL VPN Users - 'remote_management': 504, # Reboot / KVM over IP - 'os': 4166, # Ubuntu Linux 12.04 LTS Precise Pangolin (64 bit) - } - - args = { - 'location': 'FIRST_AVAILABLE', # Pick the first available DC - 'packageId': 32, # From above - 'disks': [], - } - - for cat, item_id in selections: - for item in options['categories'][cat]['items'].items(): - if item['id'] == item_id: - if 'disk' not in cat or 'disk_controller' == cat: - args[cat] = item['price_id'] - else: - args['disks'].append(item['price_id']) - - # You can call :func:`verify_order` here to test the order instead - # of actually placing it if you prefer. - result = mgr.place_order(**args) """ create_options = self._generate_create_dict(**kwargs) return self.client['Product_Order'].placeOrder(create_options) @@ -613,7 +554,7 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, # Change the hostname on instance 12345 to 'something' result = mgr.edit(hardware_id=12345 , hostname="something") #result will be True or an Exception - print result + """ @@ -656,7 +597,7 @@ def update_firmware(self, # Check the servers active transactions to see progress result = mgr.update_firmware(hardware_id=1234) - print result + """ return self.hardware.createFirmwareUpdateTransaction( diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index 3000a7ab7..1a4bde361 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -76,7 +76,7 @@ def remove_certificate(self, cert_id): # Removes certificate with id 1234 result = mgr.remove_certificate(cert_id = 1234) - print result + """ return self.ssl.deleteObject(id=cert_id) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 8b93c77da..5fc45a8cd 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -586,7 +586,7 @@ def change_port_speed(self, instance_id, public, speed): result = mgr.change_port_speed(instance_id=12345, public=True, speed=10) # result will be True or an Exception - print result + """ if public: func = self.guest.setPublicNetworkInterfaceSpeed @@ -637,7 +637,7 @@ def edit(self, instance_id, userdata=None, hostname=None, domain=None, # Change the hostname on instance 12345 to 'something' result = mgr.edit(instance_id=12345 , hostname="something") #result will be True or an Exception - print result + """ obj = {} @@ -690,7 +690,7 @@ def capture(self, instance_id, name, additional_disks=False, notes=None): name = "Testing Images" notes = "Some notes about this image" result = mgr.capture(instance_id=12345, name=name, notes=notes) - print result + """ vsi = self.get_instance(instance_id) From 7958a6e60e6b65ccf2aeef275383914487287369 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 15 Apr 2015 15:56:08 -0500 Subject: [PATCH 09/57] minor changes x2 --- SoftLayer/managers/hardware.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 2878e8add..3cb49c015 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -93,11 +93,7 @@ def cancel_metal(self, hardware_id, immediate=False): :param int id: The ID of the bare metal instance to be cancelled. :param bool immediate: If true, the bare metal instance will be cancelled immediately. Otherwise, it will be - scheduled to cancel on the anniversary date. - Example:: - - result = mgr.cancel_metal(hardware_id=1234) - + scheduled to cancel on the anniversary date. """ hw_billing = self.get_hardware(hardware_id, mask='mask[id, billingItem.id]') From 3525b36161a58e1767831acd3483e84278522355 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 15 Apr 2015 16:20:05 -0500 Subject: [PATCH 10/57] fixed tox errors --- SoftLayer/managers/hardware.py | 11 +---------- SoftLayer/managers/ssl.py | 2 -- SoftLayer/managers/vs.py | 3 --- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 3cb49c015..097d7b4df 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -59,7 +59,6 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', # Cancels hardware id 1234 result = mrg.cancel_hardware(hardware_id=1234) - """ # Check to see if this is actually a pre-configured server (BMC). They # require a different cancellation call. @@ -93,7 +92,7 @@ def cancel_metal(self, hardware_id, immediate=False): :param int id: The ID of the bare metal instance to be cancelled. :param bool immediate: If true, the bare metal instance will be cancelled immediately. Otherwise, it will be - scheduled to cancel on the anniversary date. + scheduled to cancel on the anniversary date. """ hw_billing = self.get_hardware(hardware_id, mask='mask[id, billingItem.id]') @@ -132,8 +131,6 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, # These will stem from the SoftLayer_Hardware_Server datatype object_mask = "mask[hostname,monitoringRobot[robotStatus]]" result = mgr.list_hardware(mask=object_mask) - - """ if 'mask' not in kwargs: hw_items = [ @@ -208,7 +205,6 @@ def get_hardware(self, hardware_id, **kwargs): object_mask = "mask[id,networkVlans[vlanNumber]]" # Object masks are optional result = mrg.get_hardware(hardware_id=1234,mask=object_mask) - """ if 'mask' not in kwargs: @@ -280,7 +276,6 @@ def rescue(self, hardware_id): Example:: result = mgr.rescue(1234) - """ return self.hardware.bootToRescueLayer(id=hardware_id) @@ -301,7 +296,6 @@ def change_port_speed(self, hardware_id, public, speed): result = mgr.change_port_speed(hardware_id=12345, public=True, speed=10) # result will be True or an Exception - """ if public: func = self.hardware.setPublicNetworkInterfaceSpeed @@ -550,8 +544,6 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, # Change the hostname on instance 12345 to 'something' result = mgr.edit(hardware_id=12345 , hostname="something") #result will be True or an Exception - - """ obj = {} @@ -593,7 +585,6 @@ def update_firmware(self, # Check the servers active transactions to see progress result = mgr.update_firmware(hardware_id=1234) - """ return self.hardware.createFirmwareUpdateTransaction( diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index 1a4bde361..1ea255258 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -76,8 +76,6 @@ def remove_certificate(self, cert_id): # Removes certificate with id 1234 result = mgr.remove_certificate(cert_id = 1234) - - """ return self.ssl.deleteObject(id=cert_id) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 5fc45a8cd..d45352e4b 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -586,7 +586,6 @@ def change_port_speed(self, instance_id, public, speed): result = mgr.change_port_speed(instance_id=12345, public=True, speed=10) # result will be True or an Exception - """ if public: func = self.guest.setPublicNetworkInterfaceSpeed @@ -637,7 +636,6 @@ def edit(self, instance_id, userdata=None, hostname=None, domain=None, # Change the hostname on instance 12345 to 'something' result = mgr.edit(instance_id=12345 , hostname="something") #result will be True or an Exception - """ obj = {} @@ -690,7 +688,6 @@ def capture(self, instance_id, name, additional_disks=False, notes=None): name = "Testing Images" notes = "Some notes about this image" result = mgr.capture(instance_id=12345, name=name, notes=notes) - """ vsi = self.get_instance(instance_id) From a52957f6a36b3640af66ea1520de743a229273ee Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 15 Apr 2015 16:35:47 -0500 Subject: [PATCH 11/57] adding in column map for powerSTate --- SoftLayer/CLI/virt/list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 24f587510..db4371841 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -55,11 +55,13 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, column_map['backend_ip'] = 'primaryBackendIpAddress' column_map['datacenter'] = 'datacenter-name' column_map['action'] = 'formatted-action' + column_map['powerState'] = 'powerState-name' for guest in guests: guest = utils.NestedDict(guest) guest['datacenter-name'] = guest['datacenter']['name'] guest['formatted-action'] = formatting.active_txn(guest) + guest['powerState-name'] =guest['powerState']['name'] row_column = [] for col in columns_clean: entry = None From 1af0ce5737d6ab68647bd3a1093d3941423c42f3 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 15 Apr 2015 16:39:05 -0500 Subject: [PATCH 12/57] fixed whitespace error --- SoftLayer/CLI/virt/list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index db4371841..ccb7b36cf 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -61,7 +61,7 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, guest = utils.NestedDict(guest) guest['datacenter-name'] = guest['datacenter']['name'] guest['formatted-action'] = formatting.active_txn(guest) - guest['powerState-name'] =guest['powerState']['name'] + guest['powerState-name'] = guest['powerState']['name'] row_column = [] for col in columns_clean: entry = None From 9d6fb2cb1fe5d5bb4368d46feffc7508159adaf6 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Tue, 28 Apr 2015 15:53:35 -0500 Subject: [PATCH 13/57] doc fixes --- SoftLayer/managers/hardware.py | 7 +++++-- SoftLayer/managers/ssl.py | 1 + SoftLayer/managers/vs.py | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 097d7b4df..f152cfb5a 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -27,6 +27,7 @@ class HardwareManager(utils.IdentifierMixin, object): If none is provided, one will be auto initialized. Example:: + # Initialize the Manager. # env variables. These can also be specified in ~/.softlayer, # or passed directly to SoftLayer.Client() @@ -58,7 +59,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', Example:: # Cancels hardware id 1234 - result = mrg.cancel_hardware(hardware_id=1234) + result = mgr.cancel_hardware(hardware_id=1234) """ # Check to see if this is actually a pre-configured server (BMC). They # require a different cancellation call. @@ -204,7 +205,7 @@ def get_hardware(self, hardware_id, **kwargs): object_mask = "mask[id,networkVlans[vlanNumber]]" # Object masks are optional - result = mrg.get_hardware(hardware_id=1234,mask=object_mask) + result = mgr.get_hardware(hardware_id=1234,mask=object_mask) """ if 'mask' not in kwargs: @@ -292,6 +293,7 @@ def change_port_speed(self, hardware_id, public, speed): A port speed of 0 will disable the interface. Example:: + #change the Public interface to 10Mbps on instance 12345 result = mgr.change_port_speed(hardware_id=12345, public=True, speed=10) @@ -541,6 +543,7 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, :param string notes: notes about this particular hardware Example:: + # Change the hostname on instance 12345 to 'something' result = mgr.edit(hardware_id=12345 , hostname="something") #result will be True or an Exception diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index 1ea255258..b4bada5a2 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -13,6 +13,7 @@ class SSLManager(object): :param SoftLayer.API.Client client: an API client instance Example:: + # Initialize the Manager. # env variables. These can also be specified in ~/.softlayer, # or passed directly to SoftLayer.Client() diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index d45352e4b..73283efd9 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -25,6 +25,7 @@ class VSManager(utils.IdentifierMixin, object): auto initialized. Example:: + # Initialize the VSManager. # env variables. These can also be specified in ~/.softlayer, # or passed directly to SoftLayer.Client() @@ -390,6 +391,7 @@ def wait_for_ready(self, instance_id, limit, delay=1, pending=False): provisioning or reloads such as monitoring. Example:: + # Will return once vsi 12345 is ready, or after 10 checks ready = mgr.wait_for_ready(12345, 10) """ @@ -433,6 +435,7 @@ def verify_create_instance(self, **kwargs): See :func:`create_instance` for a list of available options. Example:: + new_vsi = { 'domain': u'test01.labs.sftlyr.ws', 'hostname': u'minion05', From c9e344585b0b2a1f9b1f3f967bac8b651e15ec00 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 29 Apr 2015 13:23:24 -0500 Subject: [PATCH 14/57] removing trailing whitespace on empty lines --- SoftLayer/managers/hardware.py | 2 +- SoftLayer/managers/ssl.py | 2 +- SoftLayer/managers/vs.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 8688a0703..7fe00740f 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -514,7 +514,7 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, :param string notes: notes about this particular hardware Example:: - + # Change the hostname on instance 12345 to 'something' result = mgr.edit(hardware_id=12345 , hostname="something") #result will be True or an Exception diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index b4bada5a2..9b698015c 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -13,7 +13,7 @@ class SSLManager(object): :param SoftLayer.API.Client client: an API client instance Example:: - + # Initialize the Manager. # env variables. These can also be specified in ~/.softlayer, # or passed directly to SoftLayer.Client() diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 150ea3bf9..ed3f744d8 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -435,7 +435,7 @@ def verify_create_instance(self, **kwargs): See :func:`create_instance` for a list of available options. Example:: - + new_vsi = { 'domain': u'test01.labs.sftlyr.ws', 'hostname': u'minion05', From cf0147875db6bf61ad9729572f0317fcfa23cc36 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 6 Jul 2015 14:18:09 -0500 Subject: [PATCH 15/57] Handle "empty" tag references There are apparantly tag references which don't point to a valid tag. Resolves #570. --- SoftLayer/CLI/server/detail.py | 6 ++++-- SoftLayer/CLI/virt/detail.py | 6 ++++-- SoftLayer/tests/CLI/modules/server_tests.py | 19 +++++++++++++++++++ SoftLayer/tests/CLI/modules/vs_tests.py | 19 +++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/SoftLayer/CLI/server/detail.py b/SoftLayer/CLI/server/detail.py index 943b694be..9fc76c5d8 100644 --- a/SoftLayer/CLI/server/detail.py +++ b/SoftLayer/CLI/server/detail.py @@ -96,8 +96,10 @@ def cli(env, identifier, passwords, price): table.add_row(['remote users', pass_table]) tag_row = [] - for tag in result['tagReferences']: - tag_row.append(tag['tag']['name']) + for tag_detail in result['tagReferences']: + tag = utils.lookup(tag_detail, 'tag', 'name') + if tag is not None: + tag_row.append(tag) if tag_row: table.add_row(['tags', formatting.listing(tag_row, separator=',')]) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 3afe151e2..4fc115cea 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -93,8 +93,10 @@ def cli(self, identifier, passwords=False, price=False): table.add_row(['users', pass_table]) tag_row = [] - for tag in result['tagReferences']: - tag_row.append(tag['tag']['name']) + for tag_detail in result['tagReferences']: + tag = utils.lookup(tag_detail, 'tag', 'name') + if tag is not None: + tag_row.append(tag) if tag_row: table.add_row(['tags', formatting.listing(tag_row, separator=', ')]) diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index 85df2f425..e41e49cdc 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -57,6 +57,25 @@ def test_server_details(self): self.assertEqual(result.exit_code, 0) self.assertEqual(json.loads(result.output), expected) + def test_detail_vs_empty_tag(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = { + 'id': 100, + 'processorPhysicalCoreAmount': 2, + 'memoryCapacity': 2, + 'tagReferences': [ + {'tag': {'name': 'example-tag'}}, + {}, + ], + } + result = self.run_command(['server', 'detail', '100']) + + self.assertEqual(result.exit_code, 0) + self.assertEqual( + json.loads(result.output)['tags'], + ['example-tag'], + ) + def test_list_servers(self): result = self.run_command(['server', 'list', '--tag=openstack']) diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 9c1064bde..76dd75e5f 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -66,6 +66,25 @@ def test_detail_vs(self): 'id': 1}], 'owner': 'chechu'}) + def test_detail_vs_empty_tag(self): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + 'id': 100, + 'maxCpu': 2, + 'maxMemory': 1024, + 'tagReferences': [ + {'tag': {'name': 'example-tag'}}, + {}, + ], + } + result = self.run_command(['vs', 'detail', '100']) + + self.assertEqual(result.exit_code, 0) + self.assertEqual( + json.loads(result.output)['tags'], + ['example-tag'], + ) + def test_create_options(self): result = self.run_command(['vs', 'create-options']) From c22c8ae034620f095ec0af514f95b36192eb4c32 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 7 Jul 2015 14:28:08 -0500 Subject: [PATCH 16/57] Fixes resume command --- SoftLayer/CLI/virt/power.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SoftLayer/CLI/virt/power.py b/SoftLayer/CLI/virt/power.py index 38656eef0..c0d12f92c 100644 --- a/SoftLayer/CLI/virt/power.py +++ b/SoftLayer/CLI/virt/power.py @@ -101,6 +101,7 @@ def pause(env, identifier): @click.command() @click.argument('identifier') +@environment.pass_env def resume(env, identifier): """Resumes a paused virtual server.""" From 7cdbef9670e34541044ee7c885dbdfd194ff1f86 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 7 Jul 2015 16:49:23 -0500 Subject: [PATCH 17/57] Prices can and are restricted by location This change takes that into account for hardware ordering and, thus, fixes hardware ordering... for now. --- SoftLayer/managers/hardware.py | 83 ++++++++++++++++------ SoftLayer/tests/managers/hardware_tests.py | 13 ++-- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 70aea1ec1..e8d52e838 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -360,7 +360,7 @@ def _get_package(self): prices ], activePresets, -regions[location[location]] +regions[location[location[priceGroups]]] ''' package_type = 'BARE_METAL_CPU_FAST_PROVISION' @@ -390,25 +390,32 @@ def _generate_create_dict(self, extras = extras or [] package = self._get_package() + location = _get_location(package, location) prices = [] for category in ['pri_ip_addresses', 'vpn_management', 'remote_management']: prices.append(_get_default_price_id(package['items'], - category, - hourly)) + option=category, + hourly=hourly, + location=location)) - prices.append(_get_os_price_id(package['items'], os)) + prices.append(_get_os_price_id(package['items'], os, + location=location)) prices.append(_get_bandwidth_price_id(package['items'], hourly=hourly, - no_public=no_public)) + no_public=no_public, + location=location)) prices.append(_get_port_speed_price_id(package['items'], port_speed, - no_public)) + no_public, + location=location)) for extra in extras: - prices.append(_get_extra_price_id(package['items'], extra, hourly)) + prices.append(_get_extra_price_id(package['items'], + extra, hourly, + location=location)) hardware = { 'hostname': hostname, @@ -424,7 +431,7 @@ def _generate_create_dict(self, order = { 'hardware': [hardware], - 'location': _get_location_key(package, location), + 'location': location['keyname'], 'prices': [{'id': price} for price in prices], 'packageId': package['id'], 'presetId': _get_preset_id(package, size), @@ -517,7 +524,7 @@ def update_firmware(self, id=hardware_id) -def _get_extra_price_id(items, key_name, hourly): +def _get_extra_price_id(items, key_name, hourly, location): """Returns a price id attached to item with the given key_name.""" for item in items: @@ -525,14 +532,19 @@ def _get_extra_price_id(items, key_name, hourly): continue for price in item['prices']: - if _matches_billing(price, hourly): - return price['id'] + if not _matches_billing(price, hourly): + continue + + if not _matches_location(price, location): + continue + + return price['id'] raise SoftLayer.SoftLayerError( "Could not find valid price for extra option, '%s'" % key_name) -def _get_default_price_id(items, option, hourly): +def _get_default_price_id(items, option, hourly, location): """Returns a 'free' price id given an option.""" for item in items: @@ -542,14 +554,18 @@ def _get_default_price_id(items, option, hourly): for price in item['prices']: if all([float(price.get('hourlyRecurringFee', 0)) == 0.0, float(price.get('recurringFee', 0)) == 0.0, - _matches_billing(price, hourly)]): + _matches_billing(price, hourly), + _matches_location(price, location)]): return price['id'] raise SoftLayer.SoftLayerError( "Could not find valid price for '%s' option" % option) -def _get_bandwidth_price_id(items, hourly=True, no_public=False): +def _get_bandwidth_price_id(items, + hourly=True, + no_public=False, + location=None): """Choose a valid price id for bandwidth.""" # Prefer pay-for-use data transfer with hourly @@ -565,14 +581,18 @@ def _get_bandwidth_price_id(items, hourly=True, no_public=False): continue for price in item['prices']: - if _matches_billing(price, hourly): - return price['id'] + if not _matches_billing(price, hourly): + continue + if not _matches_location(price, location): + continue + + return price['id'] raise SoftLayer.SoftLayerError( "Could not find valid price for bandwidth option") -def _get_os_price_id(items, os): +def _get_os_price_id(items, os, location): """Returns the price id matching.""" for item in items: @@ -585,13 +605,16 @@ def _get_os_price_id(items, os): continue for price in item['prices']: + if not _matches_location(price, location): + continue + return price['id'] raise SoftLayer.SoftLayerError("Could not find valid price for os: '%s'" % os) -def _get_port_speed_price_id(items, port_speed, no_public): +def _get_port_speed_price_id(items, port_speed, no_public, location): """Choose a valid price id for port speed.""" for item in items: @@ -606,6 +629,9 @@ def _get_port_speed_price_id(items, port_speed, no_public): continue for price in item['prices']: + if not _matches_location(price, location): + continue + return price['id'] raise SoftLayer.SoftLayerError( @@ -613,11 +639,26 @@ def _get_port_speed_price_id(items, port_speed, no_public): def _matches_billing(price, hourly): - """Return if the price object is hourly and/or monthly.""" + """Return True if the price object is hourly and/or monthly.""" return any([hourly and price.get('hourlyRecurringFee') is not None, not hourly and price.get('recurringFee') is not None]) +def _matches_location(price, location): + """Return True if the price object matches the location.""" + # the price has no location restriction + if not price.get('locationGroupId'): + return True + + # Check to see if any of the location groups match the location group + # of this price object + for group in location['location']['location']['priceGroups']: + if group['id'] == price['locationGroupId']: + return True + + return False + + def _is_private_port_speed_item(item): """Determine if the port speed item is private network only.""" for attribute in item['attributes']: @@ -627,11 +668,11 @@ def _is_private_port_speed_item(item): return False -def _get_location_key(package, location): +def _get_location(package, location): """Get the longer key with a short location name.""" for region in package['regions']: if region['location']['location']['name'] == location: - return region['keyname'] + return region raise SoftLayer.SoftLayerError("Could not find valid location for: '%s'" % location) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index efec2591a..38bc868d4 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -150,7 +150,8 @@ def test_generate_create_dict_no_items(self): packages.return_value = packages_copy ex = self.assertRaises(SoftLayer.SoftLayerError, - self.hardware._generate_create_dict) + self.hardware._generate_create_dict, + location="wdc01") self.assertIn("Could not find valid price", str(ex)) def test_generate_create_dict_no_regions(self): @@ -368,7 +369,7 @@ class HardwareHelperTests(testing.TestCase): def test_get_extra_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_extra_price_id, - [], 'test', True) + [], 'test', True, None) self.assertEqual("Could not find valid price for extra option, 'test'", str(ex)) @@ -384,14 +385,14 @@ def test_get_default_price_id_item_not_first(self): }] ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_default_price_id, - items, 'unknown', True) + items, 'unknown', True, None) self.assertEqual("Could not find valid price for 'unknown' option", str(ex)) def test_get_default_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_default_price_id, - [], 'test', True) + [], 'test', True, None) self.assertEqual("Could not find valid price for 'test' option", str(ex)) @@ -405,13 +406,13 @@ def test_get_bandwidth_price_id_no_items(self): def test_get_os_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_os_price_id, - [], 'UBUNTU_14_64') + [], 'UBUNTU_14_64', None) self.assertEqual("Could not find valid price for os: 'UBUNTU_14_64'", str(ex)) def test_get_port_speed_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_port_speed_price_id, - [], 10, True) + [], 10, True, None) self.assertEqual("Could not find valid price for port speed: '10'", str(ex)) From 2dfcd80965ad23276cab4e254c93b9f0ad2e05a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lalinsk=C3=BD?= Date: Wed, 8 Jul 2015 13:08:24 +0200 Subject: [PATCH 18/57] Allow editing of hardware server tags --- SoftLayer/CLI/server/edit.py | 8 +++++++- SoftLayer/managers/hardware.py | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/server/edit.py b/SoftLayer/CLI/server/edit.py index 1f867f245..d1c21d0b5 100644 --- a/SoftLayer/CLI/server/edit.py +++ b/SoftLayer/CLI/server/edit.py @@ -15,10 +15,13 @@ @click.option('--userfile', '-F', help="Read userdata from file", type=click.Path(exists=True, readable=True, resolve_path=True)) +@click.option('--tag', '-g', + multiple=True, + help="Tags to set or empty string to remove all") @click.option('--hostname', '-H', help="Host portion of the FQDN") @click.option('--userdata', '-u', help="User defined metadata string") @environment.pass_env -def cli(env, identifier, domain, userfile, hostname, userdata): +def cli(env, identifier, domain, userfile, tag, hostname, userdata): """Edit hardware details.""" if userdata and userfile: @@ -36,6 +39,9 @@ def cli(env, identifier, domain, userfile, hostname, userdata): with open(userfile, 'r') as userfile_obj: data['userdata'] = userfile_obj.read() + if tag: + data['tags'] = ','.join(tag) + mgr = SoftLayer.HardwareManager(env.client) hw_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'hardware') diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 70aea1ec1..713024959 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -462,7 +462,7 @@ def _get_ids_from_ip(self, ip): return [result['id'] for result in results] def edit(self, hardware_id, userdata=None, hostname=None, domain=None, - notes=None): + notes=None, tags=None): """Edit hostname, domain name, notes, user data of the hardware. Parameters set to None will be ignored and not attempted to be updated. @@ -473,6 +473,8 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, :param string hostname: valid hostname :param string domain: valid domain name :param string notes: notes about this particular hardware + :param string tags: tags to set on the hardware as a comma separated list. + Use the empty string to remove all tags. """ @@ -480,6 +482,9 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, if userdata: self.hardware.setUserMetadata([userdata], id=hardware_id) + if tags is not None: + self.hardware.setTags(tags, id=hardware_id) + if hostname: obj['hostname'] = hostname From 9d3ccd00e28bc199d3a3b6f06461eec56cea7e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lalinsk=C3=BD?= Date: Wed, 8 Jul 2015 13:11:42 +0200 Subject: [PATCH 19/57] Don't clear tags when running `slcli vs edit` without any `--tag` option --- SoftLayer/CLI/virt/edit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/edit.py b/SoftLayer/CLI/virt/edit.py index f296045d6..b7a7416a3 100644 --- a/SoftLayer/CLI/virt/edit.py +++ b/SoftLayer/CLI/virt/edit.py @@ -39,7 +39,9 @@ def cli(env, identifier, domain, userfile, tag, hostname, userdata): data['hostname'] = hostname data['domain'] = domain - data['tags'] = ','.join(tag) + + if tag: + data['tags'] = ','.join(tag) vsi = SoftLayer.VSManager(env.client) vs_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS') From 14ad1d61de741c120a63373ccc1dee6f52f0bae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lalinsk=C3=BD?= Date: Wed, 8 Jul 2015 13:16:55 +0200 Subject: [PATCH 20/57] Reformat the comment to fit 79 chars --- SoftLayer/managers/hardware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 713024959..9c01ef540 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -473,8 +473,8 @@ def edit(self, hardware_id, userdata=None, hostname=None, domain=None, :param string hostname: valid hostname :param string domain: valid domain name :param string notes: notes about this particular hardware - :param string tags: tags to set on the hardware as a comma separated list. - Use the empty string to remove all tags. + :param string tags: tags to set on the hardware as a comma separated + list. Use the empty string to remove all tags. """ From 20f25f6fd7edc92fa05a6d9835e4111557b9cf98 Mon Sep 17 00:00:00 2001 From: Tyler McAdams Date: Tue, 14 Jul 2015 13:57:26 -0400 Subject: [PATCH 21/57] tymac - corrected help command output for slcli subnet detail command --- SoftLayer/CLI/subnet/detail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/subnet/detail.py b/SoftLayer/CLI/subnet/detail.py index 714809c3a..931a36819 100644 --- a/SoftLayer/CLI/subnet/detail.py +++ b/SoftLayer/CLI/subnet/detail.py @@ -1,4 +1,4 @@ -"""Cancel a subnet.""" +"""Get subnet details.""" # :license: MIT, see LICENSE for more details. import SoftLayer @@ -19,7 +19,7 @@ help="Hide hardware listing") @environment.pass_env def cli(env, identifier, no_vs, no_hardware): - """Cancel a subnet.""" + """Get subnet details.""" mgr = SoftLayer.NetworkManager(env.client) subnet_id = helpers.resolve_id(mgr.resolve_subnet_ids, identifier, From 3565bb79c1742deb65c799431749da3c381a1841 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 14 Jul 2015 15:40:21 -0500 Subject: [PATCH 22/57] Remove Python 2.6 Testing. Add python 3.5. Mock, the mocking library that is used to test with has stopped support python 2.6. This is now causing travis-ci builds to fail. This change removes python 2.6 from tox and adds python 3.5 since travis-ci now supports testing the beta version of python 3.5. --- .travis.yml | 2 +- README.rst | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c8a48a798..f0e40dee7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: python python: 2.7 env: - - TOX_ENV=py26 - TOX_ENV=py27 - TOX_ENV=py33 - TOX_ENV=py34 + - TOX_ENV=py35 - TOX_ENV=pypy - TOX_ENV=analysis - TOX_ENV=coverage diff --git a/README.rst b/README.rst index 7f019fa01..0f3d8fb8e 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ library. System Requirements ------------------- -* This library has been tested on Python 2.6, 2.7, 3.3 and 3.4. +* This library has been tested on Python 2.7, 3.3 and 3.4. * A valid SoftLayer API username and key are required to call SoftLayer's API. * A connection to SoftLayer's private network is required to connect to SoftLayer’s private network API endpoints. diff --git a/tox.ini b/tox.ini index 2f693b5e8..5157016c2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,py34,pypy,analysis,coverage +envlist = py27,py33,py34,py35,pypy,analysis,coverage [testenv] setenv = From b1cf8c831f78f916e6f0ebb1dbdf4a529933b83c Mon Sep 17 00:00:00 2001 From: sudorandom Date: Fri, 17 Jul 2015 09:12:12 -0500 Subject: [PATCH 23/57] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e5e1b8adf..fe3fb196e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,7 +38,7 @@ * CLI: The command is renamed from `sl` to `slcli` to avoid package conflicts. - * CLI: Global options now need to be specified right after the `slcli` command. For example, you would now use `sl --format=raw` vs `list over sl vs list --format=raw`. This is a change for the following options: + * CLI: Global options now need to be specified right after the `slcli` command. For example, you would now use `slcli --format=raw list` over `slcli vs list --format=raw`. This is a change for the following options: * --format * -c or --config * --debug From 1d16a7666034575f73641d91579dc5e3fd439a85 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 Jul 2015 14:16:48 -0500 Subject: [PATCH 24/57] Convert to using travis-ci matrix --- .travis.yml | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index f0e40dee7..68c44ea8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,20 @@ language: python -python: 2.7 -env: - - TOX_ENV=py27 - - TOX_ENV=py33 - - TOX_ENV=py34 - - TOX_ENV=py35 - - TOX_ENV=pypy - - TOX_ENV=analysis - - TOX_ENV=coverage +matrix: + include: + - python: "2.7" + env: TOX_ENV=py27 + - python: "3.3" + env: TOX_ENV=py33 + - python: "3.4" + env: TOX_ENV=py34 + - python: "nightly" + env: TOX_ENV=py35 + - python: "pypy" + env: TOX_ENV=pypy + - python: "2.7" + env: TOX_ENV=analysis + - python: "2.7" + env: TOX_ENV=coverage install: - pip install tox --use-mirrors - pip install coveralls From edc5e59de6f933bdd010afc09fd592742e82c4bf Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 Jul 2015 14:23:32 -0500 Subject: [PATCH 25/57] Remove --use-mirrors option --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 68c44ea8c..1311f8ebc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ matrix: - python: "2.7" env: TOX_ENV=coverage install: - - pip install tox --use-mirrors + - pip install tox - pip install coveralls script: - tox -e $TOX_ENV From 80c1f455e639f4f030a406fc7df6bc4744dc60e3 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 Jul 2015 14:46:11 -0500 Subject: [PATCH 26/57] Fixes broken test --- SoftLayer/tests/CLI/helper_tests.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 873d58a89..9bc74c34a 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -7,7 +7,7 @@ """ import json import os -import sys +import tempfile import mock @@ -17,11 +17,6 @@ from SoftLayer.CLI import template from SoftLayer import testing -if sys.version_info >= (3,): - open_path = 'builtins.open' -else: - open_path = '__builtin__.open' - class CLIJSONEncoderTest(testing.TestCase): def test_default(self): @@ -403,8 +398,9 @@ def test_template_options(self): class TestExportToTemplate(testing.TestCase): def test_export_to_template(self): - with mock.patch(open_path, mock.mock_open(), create=True) as open_: - template.export_to_template('filename', { + with tempfile.NamedTemporaryFile() as tmp: + + template.export_to_template(tmp.name, { 'os': None, 'datacenter': 'ams01', 'disk': ('disk1', 'disk2'), @@ -417,8 +413,9 @@ def test_export_to_template(self): 'test': 'test', }, exclude=['test']) - open_.assert_called_with('filename', 'w') - open_().write.assert_has_calls([ - mock.call('datacenter=ams01\n'), - mock.call('disk=disk1,disk2\n'), - ], any_order=True) # Order isn't really guaranteed + with open(tmp.name) as f: + data = f.read() + + self.assertEquals(len(data.splitlines()), 2) + self.assertIn('datacenter=ams01\n', data) + self.assertIn('disk=disk1,disk2\n', data) From c6ea6da793c9466c7ea68f6a0270d3270d773cdf Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 Jul 2015 14:51:36 -0500 Subject: [PATCH 27/57] Small deprecation fix --- SoftLayer/tests/CLI/helper_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 9bc74c34a..d7222917d 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -416,6 +416,6 @@ def test_export_to_template(self): with open(tmp.name) as f: data = f.read() - self.assertEquals(len(data.splitlines()), 2) + self.assertEqual(len(data.splitlines()), 2) self.assertIn('datacenter=ams01\n', data) self.assertIn('disk=disk1,disk2\n', data) From 06a0643b204f982a395417f0f42f8523c3bec764 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 Jul 2015 14:52:39 -0500 Subject: [PATCH 28/57] Allow this to run with new travis infra --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1311f8ebc..24901049d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +sudo: false matrix: include: - python: "2.7" From bd95505d7618e88ef59bc27400487724f1829869 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 20 Jul 2015 11:29:54 -0500 Subject: [PATCH 29/57] Allow the nightly build to fail --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 24901049d..9abe38143 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,14 +8,15 @@ matrix: env: TOX_ENV=py33 - python: "3.4" env: TOX_ENV=py34 - - python: "nightly" - env: TOX_ENV=py35 - python: "pypy" env: TOX_ENV=pypy - python: "2.7" env: TOX_ENV=analysis - python: "2.7" env: TOX_ENV=coverage + allow_failures: + - python: "nightly" + env: TOX_ENV=py35 install: - pip install tox - pip install coveralls From 07cc5bf9c6ff6b37b7a3fd6159ac795540c3324f Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 23 Jul 2015 18:03:17 -0500 Subject: [PATCH 30/57] Fixes several incorrect descriptions --- SoftLayer/CLI/dns/record_remove.py | 2 +- SoftLayer/CLI/firewall/cancel.py | 4 ++-- SoftLayer/CLI/server/power.py | 2 +- SoftLayer/tests/CLI/core_tests.py | 11 +++++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/SoftLayer/CLI/dns/record_remove.py b/SoftLayer/CLI/dns/record_remove.py index 62ec3eb70..6e73790ce 100644 --- a/SoftLayer/CLI/dns/record_remove.py +++ b/SoftLayer/CLI/dns/record_remove.py @@ -13,7 +13,7 @@ @click.argument('record_id') @environment.pass_env def cli(env, record_id): - """Add resource record.""" + """Remove resource record.""" manager = SoftLayer.DNSManager(env.client) diff --git a/SoftLayer/CLI/firewall/cancel.py b/SoftLayer/CLI/firewall/cancel.py index 2953ecff2..097822b9c 100644 --- a/SoftLayer/CLI/firewall/cancel.py +++ b/SoftLayer/CLI/firewall/cancel.py @@ -1,4 +1,4 @@ -"""List firewalls.""" +"""Cancels a firewalls.""" # :license: MIT, see LICENSE for more details. import SoftLayer @@ -14,7 +14,7 @@ @click.argument('identifier') @environment.pass_env def cli(env, identifier): - """List firewalls.""" + """Cancels a firewall.""" mgr = SoftLayer.FirewallManager(env.client) firewall_type, firewall_id = firewall.parse_id(identifier) diff --git a/SoftLayer/CLI/server/power.py b/SoftLayer/CLI/server/power.py index 31a242ea5..b40c48175 100644 --- a/SoftLayer/CLI/server/power.py +++ b/SoftLayer/CLI/server/power.py @@ -66,7 +66,7 @@ def power_on(env, identifier): @click.argument('identifier') @environment.pass_env def power_cycle(env, identifier): - """Power on a server.""" + """Power cycle a server.""" mgr = SoftLayer.HardwareManager(env.client) hw_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'hardware') diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 0a7fd6fe5..550bc7f7f 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -19,7 +19,12 @@ class CoreTests(testing.TestCase): def test_load_all(self): - recursive_subcommand_loader(core.cli, path='root') + for path, cmd in recursive_subcommand_loader(core.cli, path='root'): + try: + cmd.main(args=['--help']) + except SystemExit as ex: + if ex.code != 0: + self.fail("Non-zero exit code for command: %s" % path) def test_debug_max(self): with mock.patch('logging.getLogger') as log_mock: @@ -105,4 +110,6 @@ def recursive_subcommand_loader(root, path=''): new_path = '%s:%s' % (path, command) logging.info("loading %s", new_path) new_root = root.get_command(ctx, command) - recursive_subcommand_loader(new_root, path=new_path) + for path, cmd in recursive_subcommand_loader(new_root, path=new_path): + yield path, cmd + yield path, new_root From b967825bf1d2c2a3862988dd71e4b4446390d3fc Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 23 Jul 2015 18:08:45 -0500 Subject: [PATCH 31/57] Fixes firewall cancel description --- SoftLayer/CLI/firewall/cancel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/firewall/cancel.py b/SoftLayer/CLI/firewall/cancel.py index 097822b9c..2816f1323 100644 --- a/SoftLayer/CLI/firewall/cancel.py +++ b/SoftLayer/CLI/firewall/cancel.py @@ -1,4 +1,4 @@ -"""Cancels a firewalls.""" +"""Cancels a firewall.""" # :license: MIT, see LICENSE for more details. import SoftLayer From 18854809d11b16cfc9d60e6bf1c97c78cdafc2e9 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 24 Jul 2015 14:28:22 -0500 Subject: [PATCH 32/57] Use environment.pass_env decorator --- SoftLayer/CLI/core.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 066b2173e..5e402cf19 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -72,7 +72,6 @@ def get_command(self, ctx, name): use: 'slcli setup'""", cls=CommandLoader, context_settings={'help_option_names': ['-h', '--help']}) -@click.pass_context @click.option('--format', default=DEFAULT_FORMAT, help="Output format", @@ -109,7 +108,8 @@ def get_command(self, ctx, name): required=False, help="Use fixtures instead of actually making API calls") @click.version_option(prog_name="slcli (SoftLayer Command-line)") -def cli(ctx, +@environment.pass_env +def cli(env, format='table', config=None, debug=0, @@ -131,7 +131,6 @@ def cli(ctx, 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 @@ -154,11 +153,10 @@ def cli(ctx, @cli.resultcallback() -@click.pass_context -def output_result(ctx, result, timings=False, **kwargs): +@environment.pass_env +def output_result(env, 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) From bbb49f6b575cc870b9a8466be8fe8747d004d320 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Sat, 25 Jul 2015 22:12:39 -0500 Subject: [PATCH 33/57] Version Bump to v4.0.5 --- CHANGELOG | 12 ++++++++++++ SoftLayer/consts.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fe3fb196e..71615332f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,15 @@ +4.0.5 + + * Fixes `vs resume` command + + * Updates hardware ordering to deal with location-specific prices + + * Fixes several description errors in the CLI + + * Running `vs edit` without a tag option will no longer remove all tags + + * Adds editing of hardware tags + 4.0.4 * Fixes bug with pulling the userData property for the virtual server detail diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 0279225c5..42b2739a2 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v4.0.4' +VERSION = 'v4.0.5' 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 8f480496c..f4fd443d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ # built documents. # # The short X.Y version. -version = '4.0.4' +version = '4.0.5' # The full version, including alpha/beta/rc tags. -release = '4.0.4' +release = '4.0.5' # 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 3f329b77b..0d78507c1 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( name='SoftLayer', - version='4.0.4', + version='4.0.5', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.', From 42b9bd72149d4370faaa5de7c38abfd028365fee Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 28 Jul 2015 15:26:37 -0500 Subject: [PATCH 34/57] Better comforms to click.MultiCommand.get_command Usually click.MultiCommand.get_command will return None when the command is not found. In slcli, this is handled by raising an exception. Since click handles this condition for us, there's no reason it should be done here. Removes SoftLayer.CLI.InvalidCommand and its single reference --- SoftLayer/CLI/environment.py | 5 ++--- SoftLayer/CLI/exceptions.py | 9 --------- SoftLayer/tests/CLI/environment_tests.py | 5 ++--- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index ab3d1dbe0..5965f0127 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -7,7 +7,6 @@ """ import importlib -from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting from SoftLayer.CLI import routes @@ -68,7 +67,7 @@ def list_commands(self, *path): len(path) == command.count(":")]): # offset is used to exclude the path that the caller requested. - offset = len(path_str)+1 if path_str else 0 + offset = len(path_str) + 1 if path_str else 0 commands.append(command[offset:]) return sorted(commands) @@ -80,7 +79,7 @@ def get_command(self, *path): if path_str in self.commands: return self.commands[path_str].load() - raise exceptions.InvalidCommand(path) + return None def resolve_alias(self, path_str): """Returns the actual command name. Uses the alias mapping.""" diff --git a/SoftLayer/CLI/exceptions.py b/SoftLayer/CLI/exceptions.py index 378823263..8f5917f55 100644 --- a/SoftLayer/CLI/exceptions.py +++ b/SoftLayer/CLI/exceptions.py @@ -6,8 +6,6 @@ :license: MIT, see LICENSE for more details. """ -import SoftLayer - class CLIHalt(SystemExit): """Smoothly halt the execution of the command. No error.""" @@ -34,10 +32,3 @@ class ArgumentError(CLIAbort): def __init__(self, msg, *args): super(ArgumentError, self).__init__(msg, *args) self.message = "Argument Error: %s" % msg - - -class InvalidCommand(SoftLayer.SoftLayerError): - """Raised when trying to use a command that does not exist.""" - def __init__(self, path, *args): - msg = 'Invalid command: "%s"' % ' '.join(path) - SoftLayer.SoftLayerError.__init__(self, msg, *args) diff --git a/SoftLayer/tests/CLI/environment_tests.py b/SoftLayer/tests/CLI/environment_tests.py index b3826df89..3d1f2cc74 100644 --- a/SoftLayer/tests/CLI/environment_tests.py +++ b/SoftLayer/tests/CLI/environment_tests.py @@ -9,7 +9,6 @@ import mock from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions from SoftLayer import testing @@ -30,8 +29,8 @@ def test_list_commands(self): self.assertIn('dns', actions) def test_get_command_invalid(self): - self.assertRaises(exceptions.InvalidCommand, - self.env.get_command, 'invalid', 'command') + cmd = self.env.get_command('invalid', 'command') + self.assertEqual(cmd, None) def test_get_command(self): mod_path = 'SoftLayer.tests.CLI.environment_tests' From cdfb85654d21666ababca6e75e92aa5786efae06 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 29 Jul 2015 16:43:16 -0500 Subject: [PATCH 35/57] Re-enables the use of custom headers on a per-API-call basis This was unintentionally removed some time in the past --- SoftLayer/API.py | 1 + SoftLayer/testing/xmlrpc.py | 1 + SoftLayer/tests/api_tests.py | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 09a6c63cc..2612d4128 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -223,6 +223,7 @@ def call(self, service, method, *args, **kwargs): request = self.auth.get_request(request) + request.headers.update(kwargs.get('headers', {})) return self.transport(request) __call__ = call diff --git a/SoftLayer/testing/xmlrpc.py b/SoftLayer/testing/xmlrpc.py index 47272616e..d13753b3d 100644 --- a/SoftLayer/testing/xmlrpc.py +++ b/SoftLayer/testing/xmlrpc.py @@ -49,6 +49,7 @@ def do_POST(self): 'InitParameters').get('id') req.transport_headers = dict(((k.lower(), v) for k, v in self.headers.items())) + req.headers = headers # Get response response = self.server.transport(req) diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 95272d0ae..34df1517b 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -89,9 +89,11 @@ def test_complex(self): 1234, id=5678, mask={'object': {'attribute': ''}}, + headers={'header': 'value'}, raw_headers={'RAW': 'HEADER'}, filter=_filter, - limit=9, offset=10) + limit=9, + offset=10) self.assertEqual(resp, {"test": "result"}) self.assert_called_with('SoftLayer_SERVICE', 'METHOD', @@ -102,6 +104,10 @@ def test_complex(self): limit=9, offset=10, ) + calls = self.calls('SoftLayer_SERVICE', 'METHOD') + self.assertEqual(len(calls), 1) + self.assertIn('header', calls[0].headers) + self.assertEqual(calls[0].headers['header'], 'value') @mock.patch('SoftLayer.API.BaseClient.iter_call') def test_iterate(self, _iter_call): From c5bff5fec89adc64d2afad2396756425965114b7 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 31 Jul 2015 14:10:38 -0500 Subject: [PATCH 36/57] Better handles permissions when displaying owner --- SoftLayer/CLI/server/detail.py | 13 ++++++++----- SoftLayer/CLI/virt/detail.py | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/server/detail.py b/SoftLayer/CLI/server/detail.py index 9fc76c5d8..1b9d588e1 100644 --- a/SoftLayer/CLI/server/detail.py +++ b/SoftLayer/CLI/server/detail.py @@ -62,11 +62,14 @@ def cli(env, identifier, passwords, price): 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() - )]) + if utils.lookup(result, 'billingItem') != []: + table.add_row(['owner', formatting.FormattedItem( + utils.lookup(result, 'billingItem', 'orderItem', + 'order', 'userRecord', + 'username') or formatting.blank(), + )]) + else: + table.add_row(['owner', formatting.blank()]) vlan_table = formatting.Table(['type', 'number', 'id']) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 4fc115cea..b003e4132 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -67,11 +67,14 @@ def cli(self, identifier, passwords=False, price=False): table.add_row(['private_cpu', result['dedicatedAccountHostOnlyFlag']]) table.add_row(['created', result['createDate']]) table.add_row(['modified', result['modifyDate']]) - table.add_row(['owner', formatting.FormattedItem( - utils.lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', - 'username') or formatting.blank(), - )]) + if utils.lookup(result, 'billingItem') != []: + table.add_row(['owner', formatting.FormattedItem( + utils.lookup(result, 'billingItem', 'orderItem', + 'order', 'userRecord', + 'username') or formatting.blank(), + )]) + else: + table.add_row(['owner', formatting.blank()]) vlan_table = formatting.Table(['type', 'number', 'id']) for vlan in result['networkVlans']: From 9acd37b8231642fc349c4b5babc2e554d6781f01 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 3 Aug 2015 18:00:20 -0500 Subject: [PATCH 37/57] Adds Shell / Moves fixtures This commit adds a shell that can be started by simply calling 'slcli' with no command. The shell can be exited using ctrl + d. The shell has autocomplete. The shell probably needs a way to set global options (--fixtures, --debug, --proxy, --config, --timings, etc). In the future, this mode can be used to prompt for user input. Fixtures were moved to a higher level to avoid needing test dependencies to use fixtures. --- SoftLayer/CLI/core.py | 16 +++- SoftLayer/CLI/shell.py | 93 +++++++++++++++++++ .../fixtures/SoftLayer_Account.py | 0 .../fixtures/SoftLayer_Billing_Item.py | 0 .../fixtures/SoftLayer_Billing_Order_Quote.py | 0 .../fixtures/SoftLayer_Dns_Domain.py | 0 .../SoftLayer_Dns_Domain_ResourceRecord.py | 0 .../fixtures/SoftLayer_Hardware_Server.py | 0 .../fixtures/SoftLayer_Location.py | 0 .../fixtures/SoftLayer_Location_Datacenter.py | 0 ...ntroller_LoadBalancer_Health_Check_Type.py | 0 ..._Controller_LoadBalancer_Routing_Method.py | 0 ...ry_Controller_LoadBalancer_Routing_Type.py | 0 ...elivery_Controller_LoadBalancer_Service.py | 0 ...y_Controller_LoadBalancer_Service_Group.py | 0 ...ontroller_LoadBalancer_VirtualIpAddress.py | 0 ...y_Controller_LoadBalancer_VirtualServer.py | 0 .../SoftLayer_Network_Component_Firewall.py | 0 ...ftLayer_Network_ContentDelivery_Account.py | 0 ...ftLayer_Network_Firewall_Update_Request.py | 0 .../SoftLayer_Network_Storage_Iscsi.py | 0 .../fixtures/SoftLayer_Network_Subnet.py | 0 .../SoftLayer_Network_Subnet_IpAddress.py | 0 ...ftLayer_Network_Subnet_IpAddress_Global.py | 0 .../SoftLayer_Network_Subnet_Rwhois_Data.py | 0 .../fixtures/SoftLayer_Network_Vlan.py | 0 .../SoftLayer_Network_Vlan_Firewall.py | 0 .../fixtures/SoftLayer_Product_Order.py | 0 .../fixtures/SoftLayer_Product_Package.py | 0 .../fixtures/SoftLayer_Resource_Metadata.py | 0 .../SoftLayer_Security_Certificate.py | 0 .../fixtures/SoftLayer_Security_Ssh_Key.py | 0 .../fixtures/SoftLayer_Ticket.py | 0 .../fixtures/SoftLayer_Ticket_Subject.py | 0 .../fixtures/SoftLayer_User_Customer.py | 0 .../fixtures/SoftLayer_Virtual_Guest.py | 0 ...rtual_Guest_Block_Device_Template_Group.py | 0 SoftLayer/{testing => }/fixtures/__init__.py | 0 SoftLayer/{testing => }/fixtures/empty.conf | 0 SoftLayer/{testing => }/fixtures/full.conf | 0 SoftLayer/{testing => }/fixtures/id_rsa.pub | 0 .../{testing => }/fixtures/no_options.conf | 0 SoftLayer/{testing => }/fixtures/realtest.com | 0 .../fixtures/sample_vs_template.conf | 0 SoftLayer/testing/__init__.py | 2 +- SoftLayer/tests/managers/cdn_tests.py | 2 +- SoftLayer/tests/managers/dns_tests.py | 2 +- SoftLayer/tests/managers/firewall_tests.py | 2 +- SoftLayer/tests/managers/hardware_tests.py | 2 +- SoftLayer/tests/managers/iscsi_tests.py | 2 +- SoftLayer/tests/managers/network_tests.py | 2 +- SoftLayer/tests/managers/ordering_tests.py | 2 +- SoftLayer/tests/managers/ssl_tests.py | 2 +- SoftLayer/tests/managers/ticket_tests.py | 2 +- SoftLayer/tests/managers/vs_tests.py | 2 +- SoftLayer/transports.py | 2 +- setup.py | 1 + 57 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 SoftLayer/CLI/shell.py rename SoftLayer/{testing => }/fixtures/SoftLayer_Account.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Billing_Item.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Billing_Order_Quote.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Dns_Domain.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Dns_Domain_ResourceRecord.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Hardware_Server.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Location.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Location_Datacenter.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Health_Check_Type.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Method.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Type.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service_Group.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualIpAddress.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualServer.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Component_Firewall.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_ContentDelivery_Account.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Firewall_Update_Request.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Storage_Iscsi.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Subnet.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Subnet_IpAddress.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Subnet_Rwhois_Data.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Vlan.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Network_Vlan_Firewall.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Product_Order.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Product_Package.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Resource_Metadata.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Security_Certificate.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Security_Ssh_Key.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Ticket.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Ticket_Subject.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_User_Customer.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Virtual_Guest.py (100%) rename SoftLayer/{testing => }/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py (100%) rename SoftLayer/{testing => }/fixtures/__init__.py (100%) rename SoftLayer/{testing => }/fixtures/empty.conf (100%) rename SoftLayer/{testing => }/fixtures/full.conf (100%) rename SoftLayer/{testing => }/fixtures/id_rsa.pub (100%) rename SoftLayer/{testing => }/fixtures/no_options.conf (100%) rename SoftLayer/{testing => }/fixtures/realtest.com (100%) rename SoftLayer/{testing => }/fixtures/sample_vs_template.conf (100%) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 5e402cf19..dfc87b6c5 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -1,6 +1,6 @@ """ - SoftLayer.core - ~~~~~~~~~~~~~~ + SoftLayer.CLI.core + ~~~~~~~~~~~~~~~~~~ Core for the SoftLayer CLI :license: MIT, see LICENSE for more details. @@ -71,6 +71,7 @@ def get_command(self, ctx, name): username and api_key need to be configured. The easiest way to do that is to use: 'slcli setup'""", cls=CommandLoader, + invoke_without_command=True, context_settings={'help_option_names': ['-h', '--help']}) @click.option('--format', default=DEFAULT_FORMAT, @@ -109,7 +110,8 @@ def get_command(self, ctx, name): help="Use fixtures instead of actually making API calls") @click.version_option(prog_name="slcli (SoftLayer Command-line)") @environment.pass_env -def cli(env, +@click.pass_context +def cli(ctx, env, format='table', config=None, debug=0, @@ -151,6 +153,10 @@ def cli(env, client.transport = SoftLayer.TimingTransport(client.transport) env.client = client + if ctx.invoked_subcommand is None: + from SoftLayer.CLI import shell + shell.main(env) + @cli.resultcallback() @environment.pass_env @@ -171,12 +177,12 @@ def output_result(env, result, timings=False, **kwargs): env.err(env.fmt(timing_table)) -def main(): +def main(**kwargs): """Main program. Catches several common errors and displays them nicely.""" exit_status = 0 try: - cli.main() + cli.main(**kwargs) except SoftLayer.SoftLayerAPIError as ex: if 'invalid api token' in ex.faultString.lower(): print("Authentication Failed: To update your credentials," diff --git a/SoftLayer/CLI/shell.py b/SoftLayer/CLI/shell.py new file mode 100644 index 000000000..d236c2d7f --- /dev/null +++ b/SoftLayer/CLI/shell.py @@ -0,0 +1,93 @@ +""" + SoftLayer.CLI.shell + ~~~~~~~~~~~~~~~~~~~ + An interactive shell which exposes the CLI + + :license: MIT, see LICENSE for more details. +""" +from __future__ import print_function +from __future__ import unicode_literals +import shlex +import sys +import traceback + +import click +from prompt_toolkit import completion +from prompt_toolkit import shortcuts + +from SoftLayer.CLI import core + +# pylint: disable=broad-except + + +def main(env): + """Main entry-point for the shell.""" + exit_code = 0 + while True: + try: + line = shortcuts.get_input("(%s)> " % exit_code, + completer=ShellCompleter()) + try: + args = shlex.split(line) + except ValueError as ex: + print("Invalid Command: %s" % ex) + continue + + core.main(args=args, obj=env) + except SystemExit as ex: + exit_code = ex.code + except KeyboardInterrupt: + exit_code = 1 + except EOFError: + return + except Exception: + exit_code = 1 + traceback.print_exc(file=sys.stderr) + + +class ShellCompleter(completion.Completer): + """Completer for the shell.""" + + def get_completions(self, document, complete_event): + """Returns an iterator of completions for the shell.""" + try: + parts = shlex.split(document.text_before_cursor) + except ValueError: + return [] + + return _click_generator(core.cli, parts) + + +def _click_generator(root, parts): + """Completer generator for click applications.""" + location = root + incomplete = '' + for part in parts: + incomplete = part + + if not part[0:2].isalnum(): + continue + + try: + next_location = location.get_command(click.Context(location), + part) + if next_location is not None: + location = next_location + incomplete = '' + except AttributeError: + break + + options = [] + if incomplete and not incomplete[0:2].isalnum(): + for param in location.params: + if not isinstance(param, click.Option): + continue + options.extend(param.opts) + options.extend(param.secondary_opts) + elif isinstance(location, (click.MultiCommand, click.core.Group)): + options.extend(location.list_commands(click.Context(location))) + + # yield all collected options that starts with the incomplete section + for option in options: + if option.startswith(incomplete): + yield completion.Completion(option, -len(incomplete)) diff --git a/SoftLayer/testing/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Account.py rename to SoftLayer/fixtures/SoftLayer_Account.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Billing_Item.py b/SoftLayer/fixtures/SoftLayer_Billing_Item.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Billing_Item.py rename to SoftLayer/fixtures/SoftLayer_Billing_Item.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Billing_Order_Quote.py b/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Billing_Order_Quote.py rename to SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Dns_Domain.py b/SoftLayer/fixtures/SoftLayer_Dns_Domain.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Dns_Domain.py rename to SoftLayer/fixtures/SoftLayer_Dns_Domain.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Dns_Domain_ResourceRecord.py b/SoftLayer/fixtures/SoftLayer_Dns_Domain_ResourceRecord.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Dns_Domain_ResourceRecord.py rename to SoftLayer/fixtures/SoftLayer_Dns_Domain_ResourceRecord.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Hardware_Server.py b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Hardware_Server.py rename to SoftLayer/fixtures/SoftLayer_Hardware_Server.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Location.py b/SoftLayer/fixtures/SoftLayer_Location.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Location.py rename to SoftLayer/fixtures/SoftLayer_Location.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Location_Datacenter.py b/SoftLayer/fixtures/SoftLayer_Location_Datacenter.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Location_Datacenter.py rename to SoftLayer/fixtures/SoftLayer_Location_Datacenter.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Health_Check_Type.py b/SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Health_Check_Type.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Health_Check_Type.py rename to SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Health_Check_Type.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Method.py b/SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Method.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Method.py rename to SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Method.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Type.py b/SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Type.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Type.py rename to SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Routing_Type.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service.py b/SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service.py rename to SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service_Group.py b/SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service_Group.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service_Group.py rename to SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_Service_Group.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualIpAddress.py b/SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualIpAddress.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualIpAddress.py rename to SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualIpAddress.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualServer.py b/SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualServer.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualServer.py rename to SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller_LoadBalancer_VirtualServer.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Component_Firewall.py b/SoftLayer/fixtures/SoftLayer_Network_Component_Firewall.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Component_Firewall.py rename to SoftLayer/fixtures/SoftLayer_Network_Component_Firewall.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_ContentDelivery_Account.py b/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_ContentDelivery_Account.py rename to SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Firewall_Update_Request.py b/SoftLayer/fixtures/SoftLayer_Network_Firewall_Update_Request.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Firewall_Update_Request.py rename to SoftLayer/fixtures/SoftLayer_Network_Firewall_Update_Request.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Storage_Iscsi.py b/SoftLayer/fixtures/SoftLayer_Network_Storage_Iscsi.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Storage_Iscsi.py rename to SoftLayer/fixtures/SoftLayer_Network_Storage_Iscsi.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Subnet.py b/SoftLayer/fixtures/SoftLayer_Network_Subnet.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Subnet.py rename to SoftLayer/fixtures/SoftLayer_Network_Subnet.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Subnet_IpAddress.py b/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Subnet_IpAddress.py rename to SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py b/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py rename to SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Subnet_Rwhois_Data.py b/SoftLayer/fixtures/SoftLayer_Network_Subnet_Rwhois_Data.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Subnet_Rwhois_Data.py rename to SoftLayer/fixtures/SoftLayer_Network_Subnet_Rwhois_Data.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Vlan.py b/SoftLayer/fixtures/SoftLayer_Network_Vlan.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Vlan.py rename to SoftLayer/fixtures/SoftLayer_Network_Vlan.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Network_Vlan_Firewall.py b/SoftLayer/fixtures/SoftLayer_Network_Vlan_Firewall.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Network_Vlan_Firewall.py rename to SoftLayer/fixtures/SoftLayer_Network_Vlan_Firewall.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Product_Order.py b/SoftLayer/fixtures/SoftLayer_Product_Order.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Product_Order.py rename to SoftLayer/fixtures/SoftLayer_Product_Order.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Product_Package.py b/SoftLayer/fixtures/SoftLayer_Product_Package.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Product_Package.py rename to SoftLayer/fixtures/SoftLayer_Product_Package.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Resource_Metadata.py b/SoftLayer/fixtures/SoftLayer_Resource_Metadata.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Resource_Metadata.py rename to SoftLayer/fixtures/SoftLayer_Resource_Metadata.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Security_Certificate.py b/SoftLayer/fixtures/SoftLayer_Security_Certificate.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Security_Certificate.py rename to SoftLayer/fixtures/SoftLayer_Security_Certificate.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Security_Ssh_Key.py b/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Security_Ssh_Key.py rename to SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Ticket.py b/SoftLayer/fixtures/SoftLayer_Ticket.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Ticket.py rename to SoftLayer/fixtures/SoftLayer_Ticket.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Ticket_Subject.py b/SoftLayer/fixtures/SoftLayer_Ticket_Subject.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Ticket_Subject.py rename to SoftLayer/fixtures/SoftLayer_Ticket_Subject.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_User_Customer.py b/SoftLayer/fixtures/SoftLayer_User_Customer.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_User_Customer.py rename to SoftLayer/fixtures/SoftLayer_User_Customer.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Virtual_Guest.py rename to SoftLayer/fixtures/SoftLayer_Virtual_Guest.py diff --git a/SoftLayer/testing/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py similarity index 100% rename from SoftLayer/testing/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py rename to SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py diff --git a/SoftLayer/testing/fixtures/__init__.py b/SoftLayer/fixtures/__init__.py similarity index 100% rename from SoftLayer/testing/fixtures/__init__.py rename to SoftLayer/fixtures/__init__.py diff --git a/SoftLayer/testing/fixtures/empty.conf b/SoftLayer/fixtures/empty.conf similarity index 100% rename from SoftLayer/testing/fixtures/empty.conf rename to SoftLayer/fixtures/empty.conf diff --git a/SoftLayer/testing/fixtures/full.conf b/SoftLayer/fixtures/full.conf similarity index 100% rename from SoftLayer/testing/fixtures/full.conf rename to SoftLayer/fixtures/full.conf diff --git a/SoftLayer/testing/fixtures/id_rsa.pub b/SoftLayer/fixtures/id_rsa.pub similarity index 100% rename from SoftLayer/testing/fixtures/id_rsa.pub rename to SoftLayer/fixtures/id_rsa.pub diff --git a/SoftLayer/testing/fixtures/no_options.conf b/SoftLayer/fixtures/no_options.conf similarity index 100% rename from SoftLayer/testing/fixtures/no_options.conf rename to SoftLayer/fixtures/no_options.conf diff --git a/SoftLayer/testing/fixtures/realtest.com b/SoftLayer/fixtures/realtest.com similarity index 100% rename from SoftLayer/testing/fixtures/realtest.com rename to SoftLayer/fixtures/realtest.com diff --git a/SoftLayer/testing/fixtures/sample_vs_template.conf b/SoftLayer/fixtures/sample_vs_template.conf similarity index 100% rename from SoftLayer/testing/fixtures/sample_vs_template.conf rename to SoftLayer/fixtures/sample_vs_template.conf diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py index 6a324519d..cca90f43c 100644 --- a/SoftLayer/testing/__init__.py +++ b/SoftLayer/testing/__init__.py @@ -18,7 +18,7 @@ import mock import testtools -FIXTURE_PATH = os.path.abspath(os.path.join(__file__, '..', 'fixtures')) +FIXTURE_PATH = os.path.abspath(os.path.join(__file__, '..', '..', 'fixtures')) class MockableTransport(object): diff --git a/SoftLayer/tests/managers/cdn_tests.py b/SoftLayer/tests/managers/cdn_tests.py index d78adb743..264c46719 100644 --- a/SoftLayer/tests/managers/cdn_tests.py +++ b/SoftLayer/tests/managers/cdn_tests.py @@ -7,8 +7,8 @@ import math from SoftLayer.managers import cdn +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class CDNTests(testing.TestCase): diff --git a/SoftLayer/tests/managers/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py index b59517f9e..254fbc9da 100644 --- a/SoftLayer/tests/managers/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -6,8 +6,8 @@ """ import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class DNSTests(testing.TestCase): diff --git a/SoftLayer/tests/managers/firewall_tests.py b/SoftLayer/tests/managers/firewall_tests.py index 0d056306c..730fdf29b 100644 --- a/SoftLayer/tests/managers/firewall_tests.py +++ b/SoftLayer/tests/managers/firewall_tests.py @@ -6,8 +6,8 @@ """ import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class FirewallTests(testing.TestCase): diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 38bc868d4..4e3027e51 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -9,9 +9,9 @@ import mock import SoftLayer +from SoftLayer import fixtures from SoftLayer import managers from SoftLayer import testing -from SoftLayer.testing import fixtures MINIMAL_TEST_CREATE_ARGS = { diff --git a/SoftLayer/tests/managers/iscsi_tests.py b/SoftLayer/tests/managers/iscsi_tests.py index a2cb52a41..7ba43834b 100644 --- a/SoftLayer/tests/managers/iscsi_tests.py +++ b/SoftLayer/tests/managers/iscsi_tests.py @@ -6,8 +6,8 @@ """ import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class ISCSITests(testing.TestCase): diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 959bdc247..a90abf6b6 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -5,8 +5,8 @@ :license: MIT, see LICENSE for more details. """ import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class NetworkTests(testing.TestCase): diff --git a/SoftLayer/tests/managers/ordering_tests.py b/SoftLayer/tests/managers/ordering_tests.py index dced0ec41..c1d2d29d4 100644 --- a/SoftLayer/tests/managers/ordering_tests.py +++ b/SoftLayer/tests/managers/ordering_tests.py @@ -5,8 +5,8 @@ :license: MIT, see LICENSE for more details. """ import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class OrderingTests(testing.TestCase): diff --git a/SoftLayer/tests/managers/ssl_tests.py b/SoftLayer/tests/managers/ssl_tests.py index 303a1e518..f940b3155 100644 --- a/SoftLayer/tests/managers/ssl_tests.py +++ b/SoftLayer/tests/managers/ssl_tests.py @@ -5,8 +5,8 @@ :license: MIT, see LICENSE for more details. """ import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class SSLTests(testing.TestCase): diff --git a/SoftLayer/tests/managers/ticket_tests.py b/SoftLayer/tests/managers/ticket_tests.py index 60222d114..aa75c9826 100644 --- a/SoftLayer/tests/managers/ticket_tests.py +++ b/SoftLayer/tests/managers/ticket_tests.py @@ -5,8 +5,8 @@ :license: MIT, see LICENSE for more details. """ import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class TicketTests(testing.TestCase): diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 3a179b46b..0eedcb6ad 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -7,8 +7,8 @@ import mock import SoftLayer +from SoftLayer import fixtures from SoftLayer import testing -from SoftLayer.testing import fixtures class VSTests(testing.TestCase): diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index b30d5552b..50b5956d1 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -260,7 +260,7 @@ class FixtureTransport(object): def __call__(self, call): """Load fixture from the default fixture path.""" try: - module_path = 'SoftLayer.testing.fixtures.%s' % call.service + module_path = 'SoftLayer.fixtures.%s' % call.service module = importlib.import_module(module_path) except ImportError: raise NotImplementedError('%s fixture is not implemented' diff --git a/setup.py b/setup.py index 3f329b77b..3b76a5310 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ 'prettytable >= 0.7.0', 'click', 'requests >= 2.7.0', + 'prompt_toolkit', ] if sys.version_info < (2, 7): From 5f88af656e2974007e4719342b8969d42b392078 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 4 Aug 2015 23:05:10 -0500 Subject: [PATCH 38/57] Code organization, misc changes --- SoftLayer/CLI/core.py | 13 ++--- SoftLayer/CLI/environment.py | 20 ++++---- SoftLayer/CLI/routes.py | 2 + SoftLayer/shell/__init__.py | 0 SoftLayer/shell/cmd_exit.py | 12 +++++ SoftLayer/shell/cmd_help.py | 29 ++++++++++++ SoftLayer/{CLI/shell.py => shell/core.py} | 58 +++++++++++++++++++---- SoftLayer/tests/managers/cdn_tests.py | 2 +- 8 files changed, 108 insertions(+), 28 deletions(-) create mode 100644 SoftLayer/shell/__init__.py create mode 100644 SoftLayer/shell/cmd_exit.py create mode 100644 SoftLayer/shell/cmd_help.py rename SoftLayer/{CLI/shell.py => shell/core.py} (60%) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index dfc87b6c5..c72e5eb75 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -71,7 +71,6 @@ def get_command(self, ctx, name): username and api_key need to be configured. The easiest way to do that is to use: 'slcli setup'""", cls=CommandLoader, - invoke_without_command=True, context_settings={'help_option_names': ['-h', '--help']}) @click.option('--format', default=DEFAULT_FORMAT, @@ -110,8 +109,7 @@ def get_command(self, ctx, name): help="Use fixtures instead of actually making API calls") @click.version_option(prog_name="slcli (SoftLayer Command-line)") @environment.pass_env -@click.pass_context -def cli(ctx, env, +def cli(env, format='table', config=None, debug=0, @@ -153,10 +151,6 @@ def cli(ctx, env, client.transport = SoftLayer.TimingTransport(client.transport) env.client = client - if ctx.invoked_subcommand is None: - from SoftLayer.CLI import shell - shell.main(env) - @cli.resultcallback() @environment.pass_env @@ -177,7 +171,7 @@ def output_result(env, result, timings=False, **kwargs): env.err(env.fmt(timing_table)) -def main(**kwargs): +def main(reraise_exceptions=False, **kwargs): """Main program. Catches several common errors and displays them nicely.""" exit_status = 0 @@ -198,6 +192,9 @@ def main(**kwargs): print(str(ex.message)) exit_status = ex.code except Exception: + if reraise_exceptions: + raise + import traceback print("An unexpected error has occured:") print(str(traceback.format_exc())) diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 5965f0127..0aca995cd 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -28,12 +28,15 @@ def __init__(self): self.commands = {} self.aliases = {} + self.vars = {} + self.client = None self.format = 'table' self.skip_confirmations = False - self._modules_loaded = False self.config_file = None + self._modules_loaded = False + def out(self, output, newline=True): """Outputs a string to the console (stdout).""" click.echo(output, nl=newline) @@ -92,23 +95,22 @@ def load(self): if self._modules_loaded is True: return - self._load_modules_from_python() - self._load_modules_from_entry_points() + self.load_modules_from_python(routes.ALL_ROUTES) + self.aliases.update(routes.ALL_ALIASES) + self._load_modules_from_entry_points('softlayer.cli') self._modules_loaded = True - def _load_modules_from_python(self): + def load_modules_from_python(self, route_list): """Load modules from the native python source.""" - for name, modpath in routes.ALL_ROUTES: + for name, modpath in route_list: if ':' in modpath: path, attr = modpath.split(':', 1) else: path, attr = modpath, None self.commands[name] = ModuleLoader(path, attr=attr) - self.aliases = routes.ALL_ALIASES - - def _load_modules_from_entry_points(self): + def _load_modules_from_entry_points(self, entry_point_group): """Load modules from the entry_points (slower). Entry points can be used to add new commands to the CLI. @@ -118,7 +120,7 @@ def _load_modules_from_entry_points(self): entry_points={'softlayer.cli': ['new-cmd = mymodule.new_cmd.cli']} """ - for obj in pkg_resources.iter_entry_points(group='softlayer.cli', + for obj in pkg_resources.iter_entry_points(group=entry_point_group, name=None): self.commands[obj.name] = obj diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 8035aec0b..6f2d2aec4 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -7,6 +7,8 @@ """ ALL_ROUTES = [ + ('shell', 'SoftLayer.shell.core:cli'), + ('call-api', 'SoftLayer.CLI.call_api:cli'), ('vs', 'SoftLayer.CLI.virt'), diff --git a/SoftLayer/shell/__init__.py b/SoftLayer/shell/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/SoftLayer/shell/cmd_exit.py b/SoftLayer/shell/cmd_exit.py new file mode 100644 index 000000000..1d261c372 --- /dev/null +++ b/SoftLayer/shell/cmd_exit.py @@ -0,0 +1,12 @@ +"""Exit the shell.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.shell import core + + +@click.command() +def cli(): + """Exit the shell.""" + raise core.ShellExit() diff --git a/SoftLayer/shell/cmd_help.py b/SoftLayer/shell/cmd_help.py new file mode 100644 index 000000000..d18038d8b --- /dev/null +++ b/SoftLayer/shell/cmd_help.py @@ -0,0 +1,29 @@ +"""Print help text.""" +# :license: MIT, see LICENSE for more details. + +import click +from click import formatting + +from SoftLayer.CLI import core +from SoftLayer.CLI import environment + + +@click.command() +@environment.pass_env +@click.pass_context +def cli(ctx, env): + """Print shell help text.""" + env.out("Welcome to the SoftLayer shell.") + env.out("") + + formatter = formatting.HelpFormatter() + commands = [] + for name in core.cli.list_commands(ctx): + command = core.cli.get_command(ctx, name) + commands.append((name, command.short_help)) + + with formatter.section('Available Commands'): + formatter.write_dl(commands) + + for line in formatter.buffer: + env.out(line, newline=False) diff --git a/SoftLayer/CLI/shell.py b/SoftLayer/shell/core.py similarity index 60% rename from SoftLayer/CLI/shell.py rename to SoftLayer/shell/core.py index d236c2d7f..4655a485f 100644 --- a/SoftLayer/CLI/shell.py +++ b/SoftLayer/shell/core.py @@ -7,45 +7,83 @@ """ from __future__ import print_function from __future__ import unicode_literals +import os import shlex import sys import traceback import click -from prompt_toolkit import completion -from prompt_toolkit import shortcuts +from prompt_toolkit import completion as p_completion +from prompt_toolkit import history as p_history +from prompt_toolkit import shortcuts as p_shortcuts from SoftLayer.CLI import core +from SoftLayer.CLI import environment # pylint: disable=broad-except +ALL_ROUTES = [ + ('exit', 'SoftLayer.shell.cmd_exit:cli'), + ('shell-help', 'SoftLayer.shell.cmd_help:cli'), +] -def main(env): - """Main entry-point for the shell.""" +ALL_ALIASES = { + '?': 'shell-help', + 'help': 'shell-help', + 'quit': 'exit', +} + + +class ShellExit(Exception): + pass + + +@click.command() +@environment.pass_env +@click.pass_context +def cli(ctx, env): + """Enters a shell for slcli.""" + env.load_modules_from_python(ALL_ROUTES) + env.aliases.update(ALL_ALIASES) exit_code = 0 + app_path = click.get_app_dir('softlayer') + + if not os.path.exists(app_path): + os.makedirs(os.path.dirname(app_path)) + history = p_history.FileHistory(os.path.join(app_path, 'history')) + completer = ShellCompleter() + while True: try: - line = shortcuts.get_input("(%s)> " % exit_code, - completer=ShellCompleter()) + line = p_shortcuts.get_input("(%s)> " % exit_code, + completer=completer, + history=history) try: args = shlex.split(line) except ValueError as ex: print("Invalid Command: %s" % ex) continue - core.main(args=args, obj=env) + # Reset client so that --fixtures can be toggled on and off + env.client = None + core.main(args=args, + obj=env, + prog_name="", + reraise_exceptions=True) except SystemExit as ex: exit_code = ex.code except KeyboardInterrupt: exit_code = 1 except EOFError: return - except Exception: + except ShellExit: + return + except Exception as ex: exit_code = 1 traceback.print_exc(file=sys.stderr) -class ShellCompleter(completion.Completer): +class ShellCompleter(p_completion.Completer): """Completer for the shell.""" def get_completions(self, document, complete_event): @@ -90,4 +128,4 @@ def _click_generator(root, parts): # yield all collected options that starts with the incomplete section for option in options: if option.startswith(incomplete): - yield completion.Completion(option, -len(incomplete)) + yield p_completion.Completion(option, -len(incomplete)) diff --git a/SoftLayer/tests/managers/cdn_tests.py b/SoftLayer/tests/managers/cdn_tests.py index 264c46719..e4c20f52d 100644 --- a/SoftLayer/tests/managers/cdn_tests.py +++ b/SoftLayer/tests/managers/cdn_tests.py @@ -6,8 +6,8 @@ """ import math -from SoftLayer.managers import cdn from SoftLayer import fixtures +from SoftLayer.managers import cdn from SoftLayer import testing From 230e5a81a4f8c1ed97d1ff41d7a14c1773fef350 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 4 Aug 2015 23:25:13 -0500 Subject: [PATCH 39/57] Fixes tests --- SoftLayer/shell/core.py | 4 ++-- SoftLayer/tests/CLI/core_tests.py | 12 +++++++----- tools/requirements.txt | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index 4655a485f..228e460c4 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -35,13 +35,13 @@ class ShellExit(Exception): + """Exception raised to quit the shell.""" pass @click.command() @environment.pass_env -@click.pass_context -def cli(ctx, env): +def cli(env): """Enters a shell for slcli.""" env.load_modules_from_python(ALL_ROUTES) env.aliases.update(ALL_ALIASES) diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 550bc7f7f..ee2813391 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -19,7 +19,8 @@ class CoreTests(testing.TestCase): def test_load_all(self): - for path, cmd in recursive_subcommand_loader(core.cli, path='root'): + for path, cmd in recursive_subcommand_loader(core.cli, + current_path='root'): try: cmd.main(args=['--help']) except SystemExit as ex: @@ -98,7 +99,7 @@ def test_auth_error(self, stdoutmock, climock): self.assertIn("use 'slcli config setup'", stdoutmock.getvalue()) -def recursive_subcommand_loader(root, path=''): +def recursive_subcommand_loader(root, current_path=''): """Recursively load and list every command.""" if getattr(root, 'list_commands', None) is None: @@ -107,9 +108,10 @@ def recursive_subcommand_loader(root, path=''): ctx = click.Context(root) for command in root.list_commands(ctx): - new_path = '%s:%s' % (path, command) + new_path = '%s:%s' % (current_path, command) logging.info("loading %s", new_path) new_root = root.get_command(ctx, command) - for path, cmd in recursive_subcommand_loader(new_root, path=new_path): + for path, cmd in recursive_subcommand_loader(new_root, + current_path=new_path): yield path, cmd - yield path, new_root + yield current_path, new_root diff --git a/tools/requirements.txt b/tools/requirements.txt index a1f1b298b..ceb126524 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -2,3 +2,4 @@ requests click prettytable >= 0.7.0 six >= 1.7.0 +prompt_toolkit \ No newline at end of file From be698696b6f9514273ccef31a6ce0cfcead18979 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 4 Aug 2015 23:30:25 -0500 Subject: [PATCH 40/57] Fixes tests --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5157016c2..68c7662f0 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ commands = --max-statements=60 \ --min-public-methods=0 \ --min-similarity-lines=30 - pylint SoftLayer/testing/fixtures \ + pylint SoftLayer/fixtures \ -d invalid-name \ # Fixtures don't follow proper naming conventions -d missing-docstring \ # Fixtures don't have docstrings --max-module-lines=2000 \ From 6adf851289f7a0c59bf16905ecf363144b5b1a37 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 6 Aug 2015 09:09:39 -0500 Subject: [PATCH 41/57] Preserve global shell options --- SoftLayer/CLI/core.py | 4 ++-- SoftLayer/shell/core.py | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index c72e5eb75..23ab3083c 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -98,11 +98,11 @@ def get_command(self, ctx, name): @click.option('--proxy', required=False, help="HTTP[S] proxy to be use to make API calls") -@click.option('--really', '-y', +@click.option('--really / --not-really', '-y', is_flag=True, required=False, help="Confirm all prompt actions") -@click.option('--fixtures', +@click.option('--fixtures / --no-fixtures', envvar='SL_FIXTURES', is_flag=True, required=False, diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index 228e460c4..30af9a4b7 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -41,8 +41,10 @@ class ShellExit(Exception): @click.command() @environment.pass_env -def cli(env): +@click.pass_context +def cli(ctx, env): """Enters a shell for slcli.""" + env.load_modules_from_python(ALL_ROUTES) env.aliases.update(ALL_ALIASES) exit_code = 0 @@ -52,6 +54,7 @@ def cli(env): os.makedirs(os.path.dirname(app_path)) history = p_history.FileHistory(os.path.join(app_path, 'history')) completer = ShellCompleter() + env.vars['ENV_ARGS'] = ctx.parent.params while True: try: @@ -66,7 +69,20 @@ def cli(env): # Reset client so that --fixtures can be toggled on and off env.client = None - core.main(args=args, + + env_args = [] + for arg, val in env.vars.get('ENV_ARGS', {}).items(): + if val is True: + env_args.append('--%s' % arg) + elif isinstance(val, int): + for i in range(val): + env_args.append('--%s' % arg) + elif val is None: + continue + else: + env_args.append('--%s=%s' % (arg, val)) + + core.main(args=env_args + args, obj=env, prog_name="", reraise_exceptions=True) From d315b35fefe719ea3732ae90b359fcfee5b4b478 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 6 Aug 2015 09:14:24 -0500 Subject: [PATCH 42/57] Fix small linting issue --- SoftLayer/shell/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index 30af9a4b7..d2ac696f5 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -75,7 +75,7 @@ def cli(ctx, env): if val is True: env_args.append('--%s' % arg) elif isinstance(val, int): - for i in range(val): + for _ in range(val): env_args.append('--%s' % arg) elif val is None: continue From c7e10a889aa28d5ac7c80834ecc6f431a7bcda9d Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Sun, 9 Aug 2015 22:46:27 -0500 Subject: [PATCH 43/57] Small tweeks --- SoftLayer/CLI/core.py | 7 +++---- SoftLayer/shell/core.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 23ab3083c..8469adf47 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -83,7 +83,7 @@ def get_command(self, ctx, name): type=click.Path(resolve_path=True)) @click.option('--debug', required=False, - default='0', + default=None, help="Sets the debug noise level", type=click.Choice(sorted([str(key) for key in DEBUG_LOGGING_MAP.keys()]))) @@ -121,9 +121,8 @@ def cli(env, """Main click CLI entry-point.""" # Set logging level - debug_int = int(debug) - if debug_int: - verbose = debug_int + if debug is not None: + verbose = int(debug) if verbose: logger = logging.getLogger() diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index d2ac696f5..96b00160d 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -109,10 +109,10 @@ def get_completions(self, document, complete_event): except ValueError: return [] - return _click_generator(core.cli, parts) + return _click_autocomplete(core.cli, parts) -def _click_generator(root, parts): +def _click_autocomplete(root, parts): """Completer generator for click applications.""" location = root incomplete = '' From 820eeb9adcc70853735e9ebde549126a22f70438 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 10 Aug 2015 10:49:10 -0500 Subject: [PATCH 44/57] Fixes using verify_create_instance() with tags --- SoftLayer/managers/vs.py | 1 + SoftLayer/tests/managers/vs_tests.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 30de90b35..40ea99255 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -423,6 +423,7 @@ def verify_create_instance(self, **kwargs): Without actually placing an order. See :func:`create_instance` for a list of available options. """ + kwargs.pop('tags', None) create_options = self._generate_create_dict(**kwargs) return self.guest.generateOrderTemplate(create_options) diff --git a/SoftLayer/tests/managers/vs_tests.py b/SoftLayer/tests/managers/vs_tests.py index 3a179b46b..9bdb34599 100644 --- a/SoftLayer/tests/managers/vs_tests.py +++ b/SoftLayer/tests/managers/vs_tests.py @@ -141,7 +141,7 @@ def test_reload_instance(self): def test_create_verify(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} - self.vs.verify_create_instance(test=1, verify=1) + self.vs.verify_create_instance(test=1, verify=1, tags=['test', 'tags']) create_dict.assert_called_once_with(test=1, verify=1) self.assert_called_with('SoftLayer_Virtual_Guest', From dcf66e15711e47c594f20ffac7605bfc6d1a8746 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 10 Aug 2015 10:50:55 -0500 Subject: [PATCH 45/57] Fixes the `slcli firewall add` command --- SoftLayer/CLI/firewall/add.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/firewall/add.py b/SoftLayer/CLI/firewall/add.py index 9bf4db177..3e7b0f54f 100644 --- a/SoftLayer/CLI/firewall/add.py +++ b/SoftLayer/CLI/firewall/add.py @@ -15,7 +15,7 @@ type=click.Choice(['vs', 'vlan', 'server']), help='Firewall type', required=True) -@click.option('--high-availability', '--ha', +@click.option('--ha', '--high-availability', is_flag=True, help='High available firewall option') @environment.pass_env From 7fec9ad896f81aad0ece3e33c514303a5795a581 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 10 Aug 2015 11:27:23 -0500 Subject: [PATCH 46/57] Adds prompts for virtual and hardware server creation. * Environmental variables prefixed with "SLAPI_" can be used to default CLI options. For example, this will default to always using raw formatting: ```bash export SLCLI_FORMAT=raw slcli vs list ``` * Fixes the --like option for creating virtual servers. * Changes how --like and --template options work to use more of click's feature-set. --- SoftLayer/CLI/core.py | 3 +- SoftLayer/CLI/environment.py | 4 +- SoftLayer/CLI/server/create.py | 52 ++--- SoftLayer/CLI/template.py | 42 ++-- SoftLayer/CLI/virt/create.py | 276 ++++++++++++----------- SoftLayer/managers/vs.py | 1 + SoftLayer/tests/CLI/environment_tests.py | 4 +- SoftLayer/tests/CLI/helper_tests.py | 22 +- SoftLayer/tests/CLI/modules/vs_tests.py | 6 +- 9 files changed, 213 insertions(+), 197 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 5e402cf19..13b9e9b0c 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -71,7 +71,8 @@ def get_command(self, ctx, name): username and api_key need to be configured. The easiest way to do that is to use: 'slcli setup'""", cls=CommandLoader, - context_settings={'help_option_names': ['-h', '--help']}) + context_settings={'help_option_names': ['-h', '--help'], + 'auto_envvar_prefix': 'SLCLI'}) @click.option('--format', default=DEFAULT_FORMAT, help="Output format", diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 5965f0127..aae74284c 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -46,9 +46,9 @@ def fmt(self, output): """Format output based on current the environment format.""" return formatting.format_output(output, fmt=self.format) - def input(self, prompt, default=None): + def input(self, prompt, default=None, show_default=True): """Provide a command prompt.""" - return click.prompt(prompt, default=default) + return click.prompt(prompt, default=default, show_default=show_default) def getpass(self, prompt, default=None): """Provide a password prompt.""" diff --git a/SoftLayer/CLI/server/create.py b/SoftLayer/CLI/server/create.py index 704f5b5b3..9c9a31d1c 100644 --- a/SoftLayer/CLI/server/create.py +++ b/SoftLayer/CLI/server/create.py @@ -12,12 +12,29 @@ @click.command(epilog="See 'slcli server create-options' for valid options.") -@click.option('--hostname', '-H', help="Host portion of the FQDN") -@click.option('--domain', '-D', help="Domain portion of the FQDN") -@click.option('--size', '-s', help="Hardware size") -@click.option('--os', '-o', help="OS install code") -@click.option('--datacenter', '-d', help="Datacenter shortname") -@click.option('--port-speed', type=click.INT, help="Port speeds") +@click.option('--hostname', '-H', + help="Host portion of the FQDN", + required=True, + prompt=True) +@click.option('--domain', '-D', + help="Domain portion of the FQDN", + required=True, + prompt=True) +@click.option('--size', '-s', + help="Hardware size", + required=True, + prompt=True) +@click.option('--os', '-o', help="OS install code", + required=True, + prompt=True) +@click.option('--datacenter', '-d', help="Datacenter shortname", + required=True, + prompt=True) +@click.option('--port-speed', + type=click.INT, + help="Port speeds", + required=True, + prompt=True) @click.option('--billing', type=click.Choice(['hourly', 'monthly']), default='hourly', @@ -38,6 +55,8 @@ is_flag=True, help="Do not actually create the virtual server") @click.option('--template', '-t', + is_eager=True, + callback=template.TemplateCallback(list_args=['key']), help="A template file that defaults the command-line options", type=click.Path(exists=True, readable=True, resolve_path=True)) @click.option('--export', @@ -50,10 +69,6 @@ @environment.pass_env def cli(env, **args): """Order/create a dedicated server.""" - - template.update_with_template_args(args, list_args=['key']) - _validate_args(args) - mgr = SoftLayer.HardwareManager(env.client) # Get the SSH keys @@ -127,20 +142,3 @@ def cli(env, **args): output = table return output - - -def _validate_args(args): - """Raises an ArgumentError if the given arguments are not valid.""" - missing = [] - for arg in ['size', - 'datacenter', - 'os', - 'port_speed', - 'hostname', - 'domain']: - if not args.get(arg): - missing.append(arg) - - if missing: - raise exceptions.ArgumentError('Missing required options: %s' - % ', '.join(missing)) diff --git a/SoftLayer/CLI/template.py b/SoftLayer/CLI/template.py index 866c1f0b7..e27476c5d 100644 --- a/SoftLayer/CLI/template.py +++ b/SoftLayer/CLI/template.py @@ -12,30 +12,34 @@ from SoftLayer import utils -def update_with_template_args(args, list_args=None): - """Populates arguments with arguments from the template file, if provided. +class TemplateCallback(object): + """Callback to use to populate click arguments with a template.""" - :param dict args: command-line arguments - """ - if not args.get('template'): - return + def __init__(self, list_args=None): + self.list_args = list_args or [] + + def __call__(self, ctx, param, value): + if value is None: + return - list_args = list_args or [] + config = utils.configparser.ConfigParser() + ini_str = '[settings]\n' + open( + os.path.expanduser(value), 'r').read() + ini_fp = utils.StringIO(ini_str) + config.readfp(ini_fp) - template_path = args.pop('template') + # Merge template options with the options passed in + args = {} + for key, value in config.items('settings'): + if key in self.list_args: + value = value.split(',') - config = utils.configparser.ConfigParser() - ini_str = '[settings]\n' + open( - os.path.expanduser(template_path), 'r').read() - ini_fp = utils.StringIO(ini_str) - config.readfp(ini_fp) + if not args.get(key): + args[key] = value - # Merge template options with the options passed in - for key, value in config.items('settings'): - if key in list_args: - value = value.split(',') - if not args.get(key): - args[key] = value + if ctx.default_map is None: + ctx.default_map = {} + ctx.default_map.update(args) def export_to_template(filename, args, exclude=None): diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index db09191b4..a84007a3b 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -13,20 +13,143 @@ import click +def _update_with_like_args(ctx, _, value): + """Update arguments with options taken from a currently running VS.""" + if value is None: + return + + env = ctx.ensure_object(environment.Environment) + vsi = SoftLayer.VSManager(env.client) + vs_id = helpers.resolve_id(vsi.resolve_ids, value, 'VS') + like_details = vsi.get_instance(vs_id) + like_args = { + 'hostname': like_details['hostname'], + 'domain': like_details['domain'], + 'cpu': like_details['maxCpu'], + 'memory': '%smb' % like_details['maxMemory'], + 'hourly': like_details['hourlyBillingFlag'], + 'datacenter': like_details['datacenter']['name'], + 'network': like_details['networkComponents'][0]['maxSpeed'], + 'userdata': like_details['userData'] or None, + 'postinstall': like_details.get('postInstallScriptUri'), + 'dedicated': like_details['dedicatedAccountHostOnlyFlag'], + 'private': like_details['privateNetworkOnlyFlag'], + } + + tag_refs = like_details.get('tagReferences', None) + if tag_refs is not None and len(tag_refs) > 0: + like_args['tag'] = [t['tag']['name'] for t in tag_refs] + + # Handle mutually exclusive options + like_image = utils.lookup(like_details, + 'blockDeviceTemplateGroup', + 'globalIdentifier') + like_os = utils.lookup(like_details, + 'operatingSystem', + 'softwareLicense', + 'softwareDescription', + 'referenceCode') + if like_image: + like_args['image'] = like_image + elif like_os: + like_args['os'] = like_os + + if ctx.default_map is None: + ctx.default_map = {} + ctx.default_map.update(like_args) + + +def _parse_create_args(client, args): + """Converts CLI arguments to args for VSManager.create_instance. + + :param dict args: CLI arguments + """ + data = { + "hourly": args['billing'] == 'hourly', + "cpus": args['cpu'], + "domain": args['domain'], + "hostname": args['hostname'], + "private": args['private'], + "dedicated": args['dedicated'], + "disks": args['disk'], + "local_disk": not args['san'], + } + + data["memory"] = args['memory'] + + if args.get('os'): + data['os_code'] = args['os'] + + if args.get('image'): + data['image_id'] = args['image'] + + if args.get('datacenter'): + data['datacenter'] = args['datacenter'] + + if args.get('network'): + data['nic_speed'] = args.get('network') + + if args.get('userdata'): + data['userdata'] = args['userdata'] + elif args.get('userfile'): + with open(args['userfile'], 'r') as userfile: + data['userdata'] = userfile.read() + + if args.get('postinstall'): + data['post_uri'] = args.get('postinstall') + + # Get the SSH keys + if args.get('key'): + keys = [] + for key in args.get('key'): + resolver = SoftLayer.SshKeyManager(client).resolve_ids + key_id = helpers.resolve_id(resolver, key, 'SshKey') + keys.append(key_id) + data['ssh_keys'] = keys + + if args.get('vlan_public'): + data['public_vlan'] = args['vlan_public'] + + if args.get('vlan_private'): + data['private_vlan'] = args['vlan_private'] + + if args.get('tag'): + data['tags'] = ','.join(args['tag']) + + return data + + @click.command(epilog="See 'slcli vs create-options' for valid options") -@click.option('--domain', '-D', help="Domain portion of the FQDN") -@click.option('--hostname', '-H', help="Host portion of the FQDN") -@click.option('--image', - help="Image GUID. See: 'slcli image list' for reference") -@click.option('--cpu', '-c', help="Number of CPU cores", type=click.INT) -@click.option('--memory', '-m', help="Memory in mebibytes", type=virt.MEM_TYPE) +@click.option('--hostname', '-H', + help="Host portion of the FQDN", + required=True, + prompt=True) +@click.option('--domain', '-D', + help="Domain portion of the FQDN", + required=True, + prompt=True) +@click.option('--cpu', '-c', + help="Number of CPU cores", + type=click.INT, + required=True, + prompt=True) +@click.option('--memory', '-m', + help="Memory in mebibytes", + type=virt.MEM_TYPE, + required=True, + prompt=True) +@click.option('--datacenter', '-d', + help="Datacenter shortname", + required=True, + prompt=True) @click.option('--os', '-o', help="OS install code. Tip: you can specify _LATEST") +@click.option('--image', + help="Image GUID. See: 'slcli image list' for reference") @click.option('--billing', type=click.Choice(['hourly', 'monthly']), default='hourly', help="Billing rate") -@click.option('--datacenter', '-d', help="Datacenter shortname") @click.option('--dedicated/--public', is_flag=True, help="Create a dedicated Virtual Server (Private Node)") @@ -47,11 +170,16 @@ is_flag=True, help="Forces the VS to only have access the private network") @click.option('--like', - is_flag=True, + is_eager=True, + callback=_update_with_like_args, help="Use the configuration from an existing VS") @click.option('--network', '-n', help="Network port speed in Mbps") @helpers.multi_option('--tag', '-g', help="Tags to add to the instance") @click.option('--template', '-t', + is_eager=True, + callback=template.TemplateCallback(list_args=['disk', + 'key', + 'tag']), help="A template file that defaults the command-line options", type=click.Path(exists=True, readable=True, resolve_path=True)) @click.option('--userdata', '-u', help="User defined metadata string") @@ -73,11 +201,8 @@ @environment.pass_env def cli(env, **args): """Order/create virtual servers.""" - - template.update_with_template_args(args, list_args=['disk', 'key']) vsi = SoftLayer.VSManager(env.client) - _update_with_like_args(env.client, args) - _validate_args(args) + _validate_args(env, args) # Do not create a virtual server with test or export do_create = not (args['export'] or args['test']) @@ -152,16 +277,8 @@ def cli(env, **args): return output -def _validate_args(args): +def _validate_args(env, args): """Raises an ArgumentError if the given arguments are not valid.""" - missing = [] - for arg in ['cpu', 'memory', 'hostname', 'domain']: - if not args.get(arg): - missing.append(arg) - - if missing: - raise exceptions.ArgumentError('Missing required options: %s' - % ', '.join(missing)) if all([args['userdata'], args['userfile']]): raise exceptions.ArgumentError( @@ -172,114 +289,9 @@ def _validate_args(args): raise exceptions.ArgumentError( '[-o | --os] not allowed with [--image]') - if not any(image_args): - raise exceptions.ArgumentError( - 'One of [--os | --image] is required') - - -def _update_with_like_args(env, args): - """Update arguments with options taken from a currently running VS. - - :param VSManager args: A VSManager - :param dict args: CLI arguments - """ - if args['like']: - vsi = SoftLayer.VSManager(env.client) - vs_id = helpers.resolve_id(vsi.resolve_ids, args.pop('like'), 'VS') - like_details = vsi.get_instance(vs_id) - like_args = { - 'hostname': like_details['hostname'], - 'domain': like_details['domain'], - 'cpu': like_details['maxCpu'], - 'memory': like_details['maxMemory'], - 'hourly': like_details['hourlyBillingFlag'], - 'datacenter': like_details['datacenter']['name'], - 'network': like_details['networkComponents'][0]['maxSpeed'], - 'user-data': like_details['userData'] or None, - 'postinstall': like_details.get('postInstallScriptUri'), - 'dedicated': like_details['dedicatedAccountHostOnlyFlag'], - 'private': like_details['privateNetworkOnlyFlag'], - } - - tag_refs = like_details.get('tagReferences', None) - if tag_refs is not None and len(tag_refs) > 0: - like_args['tag'] = [t['tag']['name'] for t in tag_refs] - - # Handle mutually exclusive options - like_image = utils.lookup(like_details, - 'blockDeviceTemplateGroup', - 'globalIdentifier') - like_os = utils.lookup(like_details, - 'operatingSystem', - 'softwareLicense', - 'softwareDescription', - 'referenceCode') - if like_image and not args.get('os'): - like_args['image'] = like_image - elif like_os and not args.get('image'): - like_args['os'] = like_os - - # Merge like VS options with the options passed in - for key, value in like_args.items(): - if args.get(key) in [None, False]: - args[key] = value - - -def _parse_create_args(client, args): - """Converts CLI arguments to args for VSManager.create_instance. - - :param dict args: CLI arguments - """ - data = { - "hourly": args['billing'] == 'hourly', - "cpus": args['cpu'], - "domain": args['domain'], - "hostname": args['hostname'], - "private": args['private'], - "dedicated": args['dedicated'], - "disks": args['disk'], - "local_disk": not args['san'], - } - - data["memory"] = args['memory'] - - if args.get('os'): - data['os_code'] = args['os'] - - if args.get('image'): - data['image_id'] = args['image'] - - if args.get('datacenter'): - data['datacenter'] = args['datacenter'] - - if args.get('network'): - data['nic_speed'] = args.get('network') - - if args.get('userdata'): - data['userdata'] = args['userdata'] - elif args.get('userfile'): - with open(args['userfile'], 'r') as userfile: - data['userdata'] = userfile.read() - - if args.get('postinstall'): - data['post_uri'] = args.get('postinstall') - - # Get the SSH keys - if args.get('key'): - keys = [] - for key in args.get('key'): - resolver = SoftLayer.SshKeyManager(client).resolve_ids - key_id = helpers.resolve_id(resolver, key, 'SshKey') - keys.append(key_id) - data['ssh_keys'] = keys - - if args.get('vlan_public'): - data['public_vlan'] = args['vlan_public'] - - if args.get('vlan_private'): - data['private_vlan'] = args['vlan_private'] - - if args.get('tag'): - data['tags'] = ','.join(args['tag']) - - return data + while not any([args['os'], args['image']]): + args['os'] = env.input("Operating System Code", + default="", + show_default=False) + if not args['os']: + args['image'] = env.input("Image", default="", show_default=False) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 30de90b35..a3c37ace3 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -196,6 +196,7 @@ def get_instance(self, instance_id, **kwargs): manufacturer,name,version, referenceCode]]''', 'hourlyBillingFlag', + 'userData', 'billingItem.recurringFee', 'tagReferences[id,tag[name,id]]', 'networkVlans[id,vlanNumber,networkSpace]', diff --git a/SoftLayer/tests/CLI/environment_tests.py b/SoftLayer/tests/CLI/environment_tests.py index 3d1f2cc74..e4fee1cc9 100644 --- a/SoftLayer/tests/CLI/environment_tests.py +++ b/SoftLayer/tests/CLI/environment_tests.py @@ -42,7 +42,9 @@ def test_get_command(self): @mock.patch('click.prompt') def test_input(self, prompt_mock): r = self.env.input('input') - prompt_mock.assert_called_with('input', default=None) + prompt_mock.assert_called_with('input', + default=None, + show_default=True) self.assertEqual(prompt_mock(), r) @mock.patch('click.prompt') diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index d7222917d..23a82e7c3 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -9,8 +9,10 @@ import os import tempfile +import click import mock +from SoftLayer.CLI import core from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers @@ -368,27 +370,21 @@ def test_format_output_unicode(self): class TestTemplateArgs(testing.TestCase): def test_no_template_option(self): - args = {'key': 'value'} - template.update_with_template_args(args) - self.assertEqual(args, {'key': 'value'}) + ctx = click.Context(core.cli) + template.TemplateCallback()(ctx, None, None) + self.assertIsNone(ctx.default_map) def test_template_options(self): + ctx = click.Context(core.cli) path = os.path.join(testing.FIXTURE_PATH, 'sample_vs_template.conf') - args = { - 'cpu': None, - 'memory': '32', - 'template': path, - 'hourly': False, - 'disk': [], - } - template.update_with_template_args(args, list_args=['disk']) - self.assertEqual(args, { + template.TemplateCallback(list_args=['disk'])(ctx, None, path) + self.assertEqual(ctx.default_map, { 'cpu': '4', 'datacenter': 'dal05', 'domain': 'example.com', 'hostname': 'myhost', 'hourly': 'true', - 'memory': '32', + 'memory': '1024', 'monthly': 'false', 'network': '100', 'os': 'DEBIAN_7_64', diff --git a/SoftLayer/tests/CLI/modules/vs_tests.py b/SoftLayer/tests/CLI/modules/vs_tests.py index 76dd75e5f..a8df18546 100644 --- a/SoftLayer/tests/CLI/modules/vs_tests.py +++ b/SoftLayer/tests/CLI/modules/vs_tests.py @@ -11,7 +11,7 @@ import json -class DnsTests(testing.TestCase): +class VirtTests(testing.TestCase): def test_list_vs(self): result = self.run_command(['vs', 'list', '--tags=tag']) @@ -111,6 +111,7 @@ def test_create(self, confirm_mock): '--memory=1', '--network=100', '--billing=hourly', + '--datacenter=dal05', '--tag=dev', '--tag=green']) @@ -120,7 +121,8 @@ def test_create(self, confirm_mock): 'id': 100, 'created': '2013-08-01 15:23:45'}) - args = ({'domain': 'example.com', + args = ({'datacenter': {'name': 'dal05'}, + 'domain': 'example.com', 'hourlyBillingFlag': True, 'localDiskFlag': True, 'maxMemory': 1024, From 8ca0da3f25261e26d805bb51252db1564b50ff4e Mon Sep 17 00:00:00 2001 From: Phil Jackson Date: Mon, 10 Aug 2015 14:37:07 -0500 Subject: [PATCH 47/57] Add help text for TARGET argument --- SoftLayer/CLI/firewall/add.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/firewall/add.py b/SoftLayer/CLI/firewall/add.py index 3e7b0f54f..0e7b8bebc 100644 --- a/SoftLayer/CLI/firewall/add.py +++ b/SoftLayer/CLI/firewall/add.py @@ -20,7 +20,10 @@ help='High available firewall option') @environment.pass_env def cli(env, target, firewall_type, high_availability): - """Create new firewall.""" + """Create new firewall. + + TARGET: Id of the server the firewall will protect + """ mgr = SoftLayer.FirewallManager(env.client) From f8621eab4297959136d7c99f405511a75ef3648d Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 10 Aug 2015 16:31:52 -0500 Subject: [PATCH 48/57] Adds env command --- SoftLayer/CLI/call_api.py | 61 +------------------ SoftLayer/CLI/formatting.py | 59 ++++++++++++++++++ SoftLayer/shell/cmd_env.py | 14 +++++ SoftLayer/shell/core.py | 8 ++- SoftLayer/tests/CLI/helper_tests.py | 24 ++++++++ SoftLayer/tests/CLI/modules/call_api_tests.py | 24 -------- 6 files changed, 104 insertions(+), 86 deletions(-) create mode 100644 SoftLayer/shell/cmd_env.py diff --git a/SoftLayer/CLI/call_api.py b/SoftLayer/CLI/call_api.py index e24e185d9..55062c773 100644 --- a/SoftLayer/CLI/call_api.py +++ b/SoftLayer/CLI/call_api.py @@ -31,63 +31,4 @@ def cli(env, service, method, parameters, _id, mask, limit, offset): mask=mask, limit=limit, offset=offset) - return format_api_result(result) - - -def format_api_result(value): - """Convert raw API responses to response tables.""" - if isinstance(value, list): - return format_api_list(value) - if isinstance(value, dict): - return format_api_dict(value) - return value - - -def format_api_dict(result): - """Format dictionary responses into key-value table.""" - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - for key, value in result.items(): - value = format_api_result(value) - table.add_row([key, value]) - - return table - - -def format_api_list(result): - """Format list responses into a table.""" - - if not result: - return result - - if isinstance(result[0], dict): - return format_api_list_objects(result) - - table = formatting.Table(["Value"]) - for item in result: - table.add_row([format_api_result(item)]) - return table - - -def format_api_list_objects(result): - """Format list of objects into a table.""" - - all_keys = set() - for item in result: - all_keys = all_keys.union(item.keys()) - - all_keys = sorted(all_keys) - table = formatting.Table(all_keys) - - for item in result: - values = [] - for key in all_keys: - value = format_api_result(item.get(key)) - values.append(value) - - table.add_row(values) - - return table + return formatting.iter_to_table(result) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index 6f35a9855..98b87c4b4 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -338,3 +338,62 @@ def _format_python_value(value): if hasattr(value, 'to_python'): return value.to_python() return value + + +def iter_to_table(value): + """Convert raw API responses to response tables.""" + if isinstance(value, list): + return _format_list(value) + if isinstance(value, dict): + return _format_dict(value) + return value + + +def _format_dict(result): + """Format dictionary responses into key-value table.""" + + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + for key, value in result.items(): + value = iter_to_table(value) + table.add_row([key, value]) + + return table + + +def _format_list(result): + """Format list responses into a table.""" + + if not result: + return result + + if isinstance(result[0], dict): + return _format_list_objects(result) + + table = Table(["Value"]) + for item in result: + table.add_row([iter_to_table(item)]) + return table + + +def _format_list_objects(result): + """Format list of objects into a table.""" + + all_keys = set() + for item in result: + all_keys = all_keys.union(item.keys()) + + all_keys = sorted(all_keys) + table = Table(all_keys) + + for item in result: + values = [] + for key in all_keys: + value = iter_to_table(item.get(key)) + values.append(value) + + table.add_row(values) + + return table diff --git a/SoftLayer/shell/cmd_env.py b/SoftLayer/shell/cmd_env.py new file mode 100644 index 000000000..058ef6350 --- /dev/null +++ b/SoftLayer/shell/cmd_env.py @@ -0,0 +1,14 @@ +"""Print help text.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +@click.command() +@environment.pass_env +def cli(env): + """Print shell help text.""" + return formatting.iter_to_table(env.vars) diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index 96b00160d..2c7e0f2b6 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -25,6 +25,7 @@ ALL_ROUTES = [ ('exit', 'SoftLayer.shell.cmd_exit:cli'), ('shell-help', 'SoftLayer.shell.cmd_help:cli'), + ('env', 'SoftLayer.shell.cmd_env:cli'), ] ALL_ALIASES = { @@ -54,7 +55,7 @@ def cli(ctx, env): os.makedirs(os.path.dirname(app_path)) history = p_history.FileHistory(os.path.join(app_path, 'history')) completer = ShellCompleter() - env.vars['ENV_ARGS'] = ctx.parent.params + env.vars['GLOBAL_ARGS'] = ctx.parent.params while True: try: @@ -67,11 +68,14 @@ def cli(ctx, env): print("Invalid Command: %s" % ex) continue + if not args: + continue + # Reset client so that --fixtures can be toggled on and off env.client = None env_args = [] - for arg, val in env.vars.get('ENV_ARGS', {}).items(): + for arg, val in env.vars.get('GLOBAL_ARGS', {}).items(): if val is True: env_args.append('--%s' % arg) elif isinstance(val, int): diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index d7222917d..b6ec77b0c 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -419,3 +419,27 @@ def test_export_to_template(self): self.assertEqual(len(data.splitlines()), 2) self.assertIn('datacenter=ams01\n', data) self.assertIn('disk=disk1,disk2\n', data) + + +class IterToTableTests(testing.TestCase): + + def test_format_api_dict(self): + result = formatting._format_dict({'key': 'value'}) + + self.assertIsInstance(result, formatting.Table) + self.assertEqual(result.columns, ['Name', 'Value']) + self.assertEqual(result.rows, [['key', 'value']]) + + def test_format_api_list(self): + result = formatting._format_list([{'key': 'value'}]) + + self.assertIsInstance(result, formatting.Table) + self.assertEqual(result.columns, ['key']) + self.assertEqual(result.rows, [['value']]) + + def test_format_api_list_non_objects(self): + result = formatting._format_list(['a', 'b', 'c']) + + self.assertIsInstance(result, formatting.Table) + self.assertEqual(result.columns, ['Value']) + self.assertEqual(result.rows, [['a'], ['b'], ['c']]) diff --git a/SoftLayer/tests/CLI/modules/call_api_tests.py b/SoftLayer/tests/CLI/modules/call_api_tests.py index 5a5048cae..39c8d4505 100644 --- a/SoftLayer/tests/CLI/modules/call_api_tests.py +++ b/SoftLayer/tests/CLI/modules/call_api_tests.py @@ -129,27 +129,3 @@ def test_parameters(self): self.assertEqual(result.exit_code, 0) self.assert_called_with('SoftLayer_Service', 'method', args=('arg1', '1234')) - - -class CallCliHelperTests(testing.TestCase): - - def test_format_api_dict(self): - result = call_api.format_api_dict({'key': 'value'}) - - self.assertIsInstance(result, formatting.Table) - self.assertEqual(result.columns, ['Name', 'Value']) - self.assertEqual(result.rows, [['key', 'value']]) - - def test_format_api_list(self): - result = call_api.format_api_list([{'key': 'value'}]) - - self.assertIsInstance(result, formatting.Table) - self.assertEqual(result.columns, ['key']) - self.assertEqual(result.rows, [['value']]) - - def test_format_api_list_non_objects(self): - result = call_api.format_api_list(['a', 'b', 'c']) - - self.assertIsInstance(result, formatting.Table) - self.assertEqual(result.columns, ['Value']) - self.assertEqual(result.rows, [['a'], ['b'], ['c']]) From 00cd4341ab7160458447a9e461017ca242d02d85 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 10 Aug 2015 19:20:41 -0500 Subject: [PATCH 49/57] Removes unused imports --- SoftLayer/tests/CLI/modules/call_api_tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/call_api_tests.py b/SoftLayer/tests/CLI/modules/call_api_tests.py index 39c8d4505..13dda3c39 100644 --- a/SoftLayer/tests/CLI/modules/call_api_tests.py +++ b/SoftLayer/tests/CLI/modules/call_api_tests.py @@ -4,8 +4,6 @@ :license: MIT, see LICENSE for more details. """ -from SoftLayer.CLI import call_api -from SoftLayer.CLI import formatting from SoftLayer import testing import json From 1f82761f33941e83aab51ff95a2e15cc024c6ff8 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 10 Aug 2015 21:01:30 -0500 Subject: [PATCH 50/57] Improve shell help and organize a bit --- SoftLayer/shell/cmd_env.py | 4 +-- SoftLayer/shell/cmd_help.py | 19 ++++++++++---- SoftLayer/shell/core.py | 50 +++++++++++++++++++++---------------- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/SoftLayer/shell/cmd_env.py b/SoftLayer/shell/cmd_env.py index 058ef6350..e55901baf 100644 --- a/SoftLayer/shell/cmd_env.py +++ b/SoftLayer/shell/cmd_env.py @@ -1,4 +1,4 @@ -"""Print help text.""" +"""Print environment variables.""" # :license: MIT, see LICENSE for more details. import click @@ -10,5 +10,5 @@ @click.command() @environment.pass_env def cli(env): - """Print shell help text.""" + """Print environment variables.""" return formatting.iter_to_table(env.vars) diff --git a/SoftLayer/shell/cmd_help.py b/SoftLayer/shell/cmd_help.py index d18038d8b..24e12f544 100644 --- a/SoftLayer/shell/cmd_help.py +++ b/SoftLayer/shell/cmd_help.py @@ -4,8 +4,9 @@ import click from click import formatting -from SoftLayer.CLI import core +from SoftLayer.CLI import core as cli_core from SoftLayer.CLI import environment +from SoftLayer.shell import core @click.command() @@ -18,11 +19,19 @@ def cli(ctx, env): formatter = formatting.HelpFormatter() commands = [] - for name in core.cli.list_commands(ctx): - command = core.cli.get_command(ctx, name) - commands.append((name, command.short_help)) + shell_commands = [] + for name in cli_core.cli.list_commands(ctx): + command = cli_core.cli.get_command(ctx, name) + details = (name, command.short_help) + if name in dict(core.ALL_ROUTES): + shell_commands.append(details) + else: + commands.append(details) - with formatter.section('Available Commands'): + with formatter.section('Shell Commands'): + formatter.write_dl(shell_commands) + + with formatter.section('Commands'): formatter.write_dl(commands) for line in formatter.buffer: diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index 2c7e0f2b6..565f0f187 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -7,6 +7,7 @@ """ from __future__ import print_function from __future__ import unicode_literals +import copy import os import shlex import sys @@ -46,20 +47,24 @@ class ShellExit(Exception): def cli(ctx, env): """Enters a shell for slcli.""" - env.load_modules_from_python(ALL_ROUTES) - env.aliases.update(ALL_ALIASES) - exit_code = 0 + # Set up prompt_toolkit settings app_path = click.get_app_dir('softlayer') - if not os.path.exists(app_path): os.makedirs(os.path.dirname(app_path)) history = p_history.FileHistory(os.path.join(app_path, 'history')) completer = ShellCompleter() - env.vars['GLOBAL_ARGS'] = ctx.parent.params + + # Set up the environment + env = copy.deepcopy(env) + env.load_modules_from_python(ALL_ROUTES) + env.aliases.update(ALL_ALIASES) + env.vars['gloabl_args'] = ctx.parent.params + env.vars['is_shell'] = True + env.vars['last_exit_code'] = 0 while True: try: - line = p_shortcuts.get_input("(%s)> " % exit_code, + line = p_shortcuts.get_input("(%s)> " % env.vars['last_exit_code'], completer=completer, history=history) try: @@ -74,32 +79,20 @@ def cli(ctx, env): # Reset client so that --fixtures can be toggled on and off env.client = None - env_args = [] - for arg, val in env.vars.get('GLOBAL_ARGS', {}).items(): - if val is True: - env_args.append('--%s' % arg) - elif isinstance(val, int): - for _ in range(val): - env_args.append('--%s' % arg) - elif val is None: - continue - else: - env_args.append('--%s=%s' % (arg, val)) - - core.main(args=env_args + args, + core.main(args=list(get_env_args(env)) + args, obj=env, prog_name="", reraise_exceptions=True) except SystemExit as ex: - exit_code = ex.code + env.vars['last_exit_code'] = ex.code except KeyboardInterrupt: - exit_code = 1 + env.vars['last_exit_code'] = 1 except EOFError: return except ShellExit: return except Exception as ex: - exit_code = 1 + env.vars['last_exit_code'] = 1 traceback.print_exc(file=sys.stderr) @@ -149,3 +142,16 @@ def _click_autocomplete(root, parts): for option in options: if option.startswith(incomplete): yield p_completion.Completion(option, -len(incomplete)) + + +def get_env_args(env): + for arg, val in env.vars.get('gloabl_args', {}).items(): + if val is True: + yield '--%s' % arg + elif isinstance(val, int): + for _ in range(val): + yield '--%s' % arg + elif val is None: + continue + else: + yield '--%s=%s' % (arg, val) From 741bd8eb6a18c7ea86c10d664bb82ae130e0ab67 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 10 Aug 2015 21:06:24 -0500 Subject: [PATCH 51/57] Fixes mispelling, adds doc block --- SoftLayer/shell/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index 565f0f187..c230cf178 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -58,7 +58,7 @@ def cli(ctx, env): env = copy.deepcopy(env) env.load_modules_from_python(ALL_ROUTES) env.aliases.update(ALL_ALIASES) - env.vars['gloabl_args'] = ctx.parent.params + env.vars['global_args'] = ctx.parent.params env.vars['is_shell'] = True env.vars['last_exit_code'] = 0 @@ -145,7 +145,8 @@ def _click_autocomplete(root, parts): def get_env_args(env): - for arg, val in env.vars.get('gloabl_args', {}).items(): + """Yield options to inject into the slcli command from the environment.""" + for arg, val in env.vars.get('global_args', {}).items(): if val is True: yield '--%s' % arg elif isinstance(val, int): From 56d355a94133b1271e1512157f635fc230e9fdfc Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 11 Aug 2015 14:42:36 -0500 Subject: [PATCH 52/57] Moves routes/completer to their own modules --- SoftLayer/shell/cmd_help.py | 4 +- SoftLayer/shell/completer.py | 73 +++++++++++++++++++++++++++++ SoftLayer/shell/core.py | 89 ++++++------------------------------ SoftLayer/shell/routes.py | 19 ++++++++ 4 files changed, 109 insertions(+), 76 deletions(-) create mode 100644 SoftLayer/shell/completer.py create mode 100644 SoftLayer/shell/routes.py diff --git a/SoftLayer/shell/cmd_help.py b/SoftLayer/shell/cmd_help.py index 24e12f544..eeceef068 100644 --- a/SoftLayer/shell/cmd_help.py +++ b/SoftLayer/shell/cmd_help.py @@ -6,7 +6,7 @@ from SoftLayer.CLI import core as cli_core from SoftLayer.CLI import environment -from SoftLayer.shell import core +from SoftLayer.shell import routes @click.command() @@ -23,7 +23,7 @@ def cli(ctx, env): for name in cli_core.cli.list_commands(ctx): command = cli_core.cli.get_command(ctx, name) details = (name, command.short_help) - if name in dict(core.ALL_ROUTES): + if name in dict(routes.ALL_ROUTES): shell_commands.append(details) else: commands.append(details) diff --git a/SoftLayer/shell/completer.py b/SoftLayer/shell/completer.py new file mode 100644 index 000000000..23bae861d --- /dev/null +++ b/SoftLayer/shell/completer.py @@ -0,0 +1,73 @@ +""" + SoftLayer.CLI.shell.completer + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Click completer for prompt_toolkit + + :license: MIT, see LICENSE for more details. +""" +import shlex + +from SoftLayer.CLI import core + +import click +from prompt_toolkit import completion as completion + + +class ShellCompleter(completion.Completer): + """Completer for the shell.""" + + def get_completions(self, document, complete_event): + """Returns an iterator of completions for the shell.""" + + return _click_autocomplete(core.cli, document.text_before_cursor) + + +def _click_autocomplete(root, text): + """Completer generator for click applications.""" + try: + parts = shlex.split(text) + except ValueError: + return [] + + location, incomplete = _click_resolve_command(root, parts) + + if not text.endswith(' ') and not incomplete: + return [] + + options = [] + if incomplete and not incomplete[0:2].isalnum(): + for param in location.params: + if not isinstance(param, click.Option): + continue + options.extend(param.opts) + options.extend(param.secondary_opts) + elif isinstance(location, (click.MultiCommand, click.core.Group)): + options.extend(location.list_commands(click.Context(location))) + + # collect options that starts with the incomplete section + completions = [] + for option in options: + if option.startswith(incomplete): + completions.append( + completion.Completion(option, -len(incomplete))) + return completions + + +def _click_resolve_command(root, parts): + location = root + incomplete = '' + for part in parts: + incomplete = part + + if not part[0:2].isalnum(): + continue + + try: + next_location = location.get_command(click.Context(location), + part) + if next_location is not None: + location = next_location + incomplete = '' + except AttributeError: + break + return location, incomplete diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index c230cf178..b228e08a8 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -1,6 +1,6 @@ """ - SoftLayer.CLI.shell - ~~~~~~~~~~~~~~~~~~~ + SoftLayer.CLI.shell.core + ~~~~~~~~~~~~~~~~~~~~~~~~ An interactive shell which exposes the CLI :license: MIT, see LICENSE for more details. @@ -14,27 +14,16 @@ import traceback import click -from prompt_toolkit import completion as p_completion from prompt_toolkit import history as p_history from prompt_toolkit import shortcuts as p_shortcuts from SoftLayer.CLI import core from SoftLayer.CLI import environment +from SoftLayer.shell import completer +from SoftLayer.shell import routes # pylint: disable=broad-except -ALL_ROUTES = [ - ('exit', 'SoftLayer.shell.cmd_exit:cli'), - ('shell-help', 'SoftLayer.shell.cmd_help:cli'), - ('env', 'SoftLayer.shell.cmd_env:cli'), -] - -ALL_ALIASES = { - '?': 'shell-help', - 'help': 'shell-help', - 'quit': 'exit', -} - class ShellExit(Exception): """Exception raised to quit the shell.""" @@ -47,25 +36,25 @@ class ShellExit(Exception): def cli(ctx, env): """Enters a shell for slcli.""" - # Set up prompt_toolkit settings - app_path = click.get_app_dir('softlayer') - if not os.path.exists(app_path): - os.makedirs(os.path.dirname(app_path)) - history = p_history.FileHistory(os.path.join(app_path, 'history')) - completer = ShellCompleter() - # Set up the environment env = copy.deepcopy(env) - env.load_modules_from_python(ALL_ROUTES) - env.aliases.update(ALL_ALIASES) + env.load_modules_from_python(routes.ALL_ROUTES) + env.aliases.update(routes.ALL_ALIASES) env.vars['global_args'] = ctx.parent.params env.vars['is_shell'] = True env.vars['last_exit_code'] = 0 + # Set up prompt_toolkit settings + app_path = click.get_app_dir('softlayer') + if not os.path.exists(app_path): + os.makedirs(os.path.dirname(app_path)) + history = p_history.FileHistory(os.path.join(app_path, 'history')) + complete = completer.ShellCompleter() + while True: try: line = p_shortcuts.get_input("(%s)> " % env.vars['last_exit_code'], - completer=completer, + completer=complete, history=history) try: args = shlex.split(line) @@ -76,7 +65,7 @@ def cli(ctx, env): if not args: continue - # Reset client so that --fixtures can be toggled on and off + # Reset client so that the client gets refreshed env.client = None core.main(args=list(get_env_args(env)) + args, @@ -96,54 +85,6 @@ def cli(ctx, env): traceback.print_exc(file=sys.stderr) -class ShellCompleter(p_completion.Completer): - """Completer for the shell.""" - - def get_completions(self, document, complete_event): - """Returns an iterator of completions for the shell.""" - try: - parts = shlex.split(document.text_before_cursor) - except ValueError: - return [] - - return _click_autocomplete(core.cli, parts) - - -def _click_autocomplete(root, parts): - """Completer generator for click applications.""" - location = root - incomplete = '' - for part in parts: - incomplete = part - - if not part[0:2].isalnum(): - continue - - try: - next_location = location.get_command(click.Context(location), - part) - if next_location is not None: - location = next_location - incomplete = '' - except AttributeError: - break - - options = [] - if incomplete and not incomplete[0:2].isalnum(): - for param in location.params: - if not isinstance(param, click.Option): - continue - options.extend(param.opts) - options.extend(param.secondary_opts) - elif isinstance(location, (click.MultiCommand, click.core.Group)): - options.extend(location.list_commands(click.Context(location))) - - # yield all collected options that starts with the incomplete section - for option in options: - if option.startswith(incomplete): - yield p_completion.Completion(option, -len(incomplete)) - - def get_env_args(env): """Yield options to inject into the slcli command from the environment.""" for arg, val in env.vars.get('global_args', {}).items(): diff --git a/SoftLayer/shell/routes.py b/SoftLayer/shell/routes.py new file mode 100644 index 000000000..45996bd75 --- /dev/null +++ b/SoftLayer/shell/routes.py @@ -0,0 +1,19 @@ +""" + SoftLayer.CLI.routes + ~~~~~~~~~~~~~~~~~~~ + Routes for shell-specific commands + + :license: MIT, see LICENSE for more details. +""" + +ALL_ROUTES = [ + ('exit', 'SoftLayer.shell.cmd_exit:cli'), + ('shell-help', 'SoftLayer.shell.cmd_help:cli'), + ('env', 'SoftLayer.shell.cmd_env:cli'), +] + +ALL_ALIASES = { + '?': 'shell-help', + 'help': 'shell-help', + 'quit': 'exit', +} From e1656a417a5e97f2fd615b3ad91be9fd1190db9f Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 11 Aug 2015 14:47:58 -0500 Subject: [PATCH 53/57] Adds missing doc block --- SoftLayer/shell/completer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SoftLayer/shell/completer.py b/SoftLayer/shell/completer.py index 23bae861d..8ed059a1f 100644 --- a/SoftLayer/shell/completer.py +++ b/SoftLayer/shell/completer.py @@ -54,6 +54,7 @@ def _click_autocomplete(root, text): def _click_resolve_command(root, parts): + """Return the click command and the left over text given some vargs.""" location = root incomplete = '' for part in parts: From e429afb1c9934fba57f2cc8e0b11efbe0b90d5cc Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 11 Aug 2015 15:01:34 -0500 Subject: [PATCH 54/57] Fix app path creation --- SoftLayer/shell/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index b228e08a8..8c5151d2e 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -47,7 +47,7 @@ def cli(ctx, env): # Set up prompt_toolkit settings app_path = click.get_app_dir('softlayer') if not os.path.exists(app_path): - os.makedirs(os.path.dirname(app_path)) + os.makedirs(app_path) history = p_history.FileHistory(os.path.join(app_path, 'history')) complete = completer.ShellCompleter() From 60a7bc29a9aced0d5bb8703bf667c0245c0def50 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 11 Aug 2015 15:07:03 -0500 Subject: [PATCH 55/57] allow tab completion w/empty prompt --- SoftLayer/shell/completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/shell/completer.py b/SoftLayer/shell/completer.py index 8ed059a1f..40f3c8439 100644 --- a/SoftLayer/shell/completer.py +++ b/SoftLayer/shell/completer.py @@ -31,7 +31,7 @@ def _click_autocomplete(root, text): location, incomplete = _click_resolve_command(root, parts) - if not text.endswith(' ') and not incomplete: + if not text.endswith(' ') and not incomplete and text: return [] options = [] From 9e4f5322e114aeae0d8b5903150aa4c4464b42c5 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 11 Aug 2015 16:35:42 -0500 Subject: [PATCH 56/57] Version Bump to 4.1.0 --- CHANGELOG | 12 +++++++++++- SoftLayer/consts.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 71615332f..346c62ef9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,14 @@ -4.0.5 +4.1.0 + + * Adds a shell which provides a shell interface for `slcli`. This is available by using `slcli shell` + + * `slcli vs create` and `slcli server create` will now prompt for missing required options + + * Fixes `slcli firewall add` command + + * Handles case where `slcli vs detail` and `slcli server detail` was causing an error when trying to display the creator + + * Fixes VSManager.verify_create_instance() with tags (and, in turn, `slcli vs create --test` with tags) * Fixes `vs resume` command diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 42b2739a2..d849541fb 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v4.0.5' +VERSION = 'v4.1.0' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/docs/conf.py b/docs/conf.py index f4fd443d0..00c951ba0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ # built documents. # # The short X.Y version. -version = '4.0.5' +version = '4.1.0' # The full version, including alpha/beta/rc tags. -release = '4.0.5' +release = '4.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 0d78507c1..397ed7395 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( name='SoftLayer', - version='4.0.5', + version='4.1.0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.', From f00dfc97e9619e53193818be30e8cfa5c02c4ea9 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 12 Aug 2015 15:16:01 -0500 Subject: [PATCH 57/57] getting everything in line with new changes --- SoftLayer/CLI/server/list.py | 15 ++++++--------- SoftLayer/CLI/virt/list.py | 4 ++-- SoftLayer/tests/CLI/modules/server_tests.py | 1 - 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/SoftLayer/CLI/server/list.py b/SoftLayer/CLI/server/list.py index b132b5ffb..9ca77093b 100644 --- a/SoftLayer/CLI/server/list.py +++ b/SoftLayer/CLI/server/list.py @@ -12,13 +12,8 @@ @click.command() -@click.option('--sortby', - help='Column to sort by', - type=click.Choice(['id', - 'hostname', - 'primary_ip', - 'backend_ip', - 'datacenter'])) +@click.option('--sortby', help='Column to sort by', + default='hostname') @click.option('--cpu', '-c', help='Filter by number of CPU cores') @click.option('--domain', '-D', help='Filter by domain') @click.option('--datacenter', '-d', help='Filter by datacenter') @@ -26,8 +21,8 @@ @click.option('--memory', '-m', help='Filter by memory in gigabytes') @click.option('--network', '-n', help='Filter by network port speed in Mbps') @click.option('--columns', help='Columns to display. default is ' - ' guid, hostname, primary_ip, backend_ip, datacenter, action', - default="guid,hostname,primary_ip,backend_ip,datacenter,action") + ' id, hostname, primary_ip, backend_ip, datacenter, action', + default="id,hostname,primary_ip,backend_ip,datacenter,action") @helpers.multi_option('--tag', help='Filter by tags') @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, @@ -53,11 +48,13 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, column_map['backend_ip'] = 'primaryBackendIpAddress' column_map['datacenter'] = 'datacenter-name' column_map['action'] = 'formatted-action' + column_map['powerState'] = 'powerState-name' for server in servers: server = utils.NestedDict(server) server['datacenter-name'] = server['datacenter']['name'] server['formatted-action'] = formatting.active_txn(server) + server['powerState-name'] = server['powerState']['name'] row_column = [] for col in columns_clean: entry = None diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index ccb7b36cf..1fc934416 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -24,8 +24,8 @@ help='Show instances that have one of these comma-separated ' 'tags') @click.option('--columns', help='Columns to display. default is ' - ' guid, hostname, primary_ip, backend_ip, datacenter, action', - default="guid,hostname,primary_ip,backend_ip,datacenter,action") + ' id, hostname, primary_ip, backend_ip, datacenter, action', + default="id,hostname,primary_ip,backend_ip,datacenter,action") @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, hourly, monthly, tags, columns): diff --git a/SoftLayer/tests/CLI/modules/server_tests.py b/SoftLayer/tests/CLI/modules/server_tests.py index b9cfaf027..e41e49cdc 100644 --- a/SoftLayer/tests/CLI/modules/server_tests.py +++ b/SoftLayer/tests/CLI/modules/server_tests.py @@ -101,7 +101,6 @@ def test_list_servers(self): 'primary_ip': '172.16.4.95', 'hostname': 'hardware-bad-memory', 'id': 1002, - 'guid': None, 'backend_ip': '10.1.0.4', 'action': None, },