Repository: softlayer/softlayer-python
Pull request #596 (2015-08-03 23:01:19)

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 (nosetests, testtools, etc) to use fixtures.

This is still a bit of a WiP.


Patch:
diff --git a/SoftLayer/CLI/call_api.py b/SoftLayer/CLI/call_api.py
index e24e185d..55062c77 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/core.py b/SoftLayer/CLI/core.py
index 5e402cf1..8469adf4 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.
@@ -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()])))
@@ -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,
@@ -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()
@@ -171,12 +170,12 @@ def output_result(env, result, timings=False, **kwargs):
         env.err(env.fmt(timing_table))
 
 
-def main():
+def main(reraise_exceptions=False, **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,"
@@ -192,6 +191,9 @@ def main():
         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 5965f012..0aca995c 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/formatting.py b/SoftLayer/CLI/formatting.py
index 6f35a985..98b87c4b 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/CLI/routes.py b/SoftLayer/CLI/routes.py
index 8035aec0..6f2d2aec 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/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/shell/__init__.py b/SoftLayer/shell/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/SoftLayer/shell/cmd_env.py b/SoftLayer/shell/cmd_env.py
new file mode 100644
index 00000000..e55901ba
--- /dev/null
+++ b/SoftLayer/shell/cmd_env.py
@@ -0,0 +1,14 @@
+"""Print environment variables."""
+# :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 environment variables."""
+    return formatting.iter_to_table(env.vars)
diff --git a/SoftLayer/shell/cmd_exit.py b/SoftLayer/shell/cmd_exit.py
new file mode 100644
index 00000000..1d261c37
--- /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 00000000..eeceef06
--- /dev/null
+++ b/SoftLayer/shell/cmd_help.py
@@ -0,0 +1,38 @@
+"""Print help text."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+from click import formatting
+
+from SoftLayer.CLI import core as cli_core
+from SoftLayer.CLI import environment
+from SoftLayer.shell import routes
+
+
+@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 = []
+    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(routes.ALL_ROUTES):
+            shell_commands.append(details)
+        else:
+            commands.append(details)
+
+    with formatter.section('Shell Commands'):
+        formatter.write_dl(shell_commands)
+
+    with formatter.section('Commands'):
+        formatter.write_dl(commands)
+
+    for line in formatter.buffer:
+        env.out(line, newline=False)
diff --git a/SoftLayer/shell/completer.py b/SoftLayer/shell/completer.py
new file mode 100644
index 00000000..40f3c843
--- /dev/null
+++ b/SoftLayer/shell/completer.py
@@ -0,0 +1,74 @@
+"""
+    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 and text:
+        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):
+    """Return the click command and the left over text given some vargs."""
+    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
new file mode 100644
index 00000000..8c5151d2
--- /dev/null
+++ b/SoftLayer/shell/core.py
@@ -0,0 +1,99 @@
+"""
+    SoftLayer.CLI.shell.core
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+    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 copy
+import os
+import shlex
+import sys
+import traceback
+
+import click
+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
+
+
+class ShellExit(Exception):
+    """Exception raised to quit the shell."""
+    pass
+
+
+@click.command()
+@environment.pass_env
+@click.pass_context
+def cli(ctx, env):
+    """Enters a shell for slcli."""
+
+    # Set up the environment
+    env = copy.deepcopy(env)
+    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(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=complete,
+                                         history=history)
+            try:
+                args = shlex.split(line)
+            except ValueError as ex:
+                print("Invalid Command: %s" % ex)
+                continue
+
+            if not args:
+                continue
+
+            # Reset client so that the client gets refreshed
+            env.client = None
+
+            core.main(args=list(get_env_args(env)) + args,
+                      obj=env,
+                      prog_name="",
+                      reraise_exceptions=True)
+        except SystemExit as ex:
+            env.vars['last_exit_code'] = ex.code
+        except KeyboardInterrupt:
+            env.vars['last_exit_code'] = 1
+        except EOFError:
+            return
+        except ShellExit:
+            return
+        except Exception as ex:
+            env.vars['last_exit_code'] = 1
+            traceback.print_exc(file=sys.stderr)
+
+
+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():
+        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)
diff --git a/SoftLayer/shell/routes.py b/SoftLayer/shell/routes.py
new file mode 100644
index 00000000..45996bd7
--- /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',
+}
diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py
index b30d5552..50b5956d 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 3f329b77..3b76a531 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):
diff --git a/tools/requirements.txt b/tools/requirements.txt
index a1f1b298..ceb12652 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
diff --git a/tox.ini b/tox.ini
index 5157016c..68c7662f 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 \


Test patch:
diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py
index 6a324519..cca90f43 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/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py
index 550bc7f7..ee281339 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/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py
index d7222917..b6ec77b0 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 5a5048ca..13dda3c3 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
@@ -129,27 +127,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']])
diff --git a/SoftLayer/tests/managers/cdn_tests.py b/SoftLayer/tests/managers/cdn_tests.py
index d78adb74..e4c20f52 100644
--- a/SoftLayer/tests/managers/cdn_tests.py
+++ b/SoftLayer/tests/managers/cdn_tests.py
@@ -6,9 +6,9 @@
 """
 import math
 
+from SoftLayer import fixtures
 from SoftLayer.managers import cdn
 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 b59517f9..254fbc9d 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 0d056306..730fdf29 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 38bc868d..4e3027e5 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 a2cb52a4..7ba43834 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 959bdc24..a90abf6b 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 dced0ec4..c1d2d29d 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 303a1e51..f940b315 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 60222d11..aa75c982 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 3a179b46..0eedcb6a 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):