Repository: softlayer/softlayer-python
Pull request #604 (2015-08-10 16:32:05)

Adds prompts for virtual and hardware server creation.
 * If a user does not pass in an option that is required for `slcli vs create` or `slcli server create` then the user will be prompted for the missing required options. They will not be given the available options (yet). I haven't figured out a good way to do that.
 * Environmental variables prefixed with "SLAPI_" can now 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. This was required in order to detect missing options properly.



Patch:
diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py
index 5e402cf1..13b9e9b0 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 5965f012..aae74284 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 704f5b5b..9c9a31d1 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 866c1f0b..e27476c5 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 db09191b..a84007a3 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 <OS>_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 30de90b3..a3c37ace 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]',


Test patch:
diff --git a/SoftLayer/tests/CLI/environment_tests.py b/SoftLayer/tests/CLI/environment_tests.py
index 3d1f2cc7..e4fee1cc 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 d7222917..23a82e7c 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 76dd75e5..a8df1854 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,