diff --git a/.travis.yml b/.travis.yml index c8a48a798..9abe38143 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,24 @@ language: python -python: 2.7 -env: - - TOX_ENV=py26 - - TOX_ENV=py27 - - TOX_ENV=py33 - - TOX_ENV=py34 - - TOX_ENV=pypy - - TOX_ENV=analysis - - TOX_ENV=coverage +sudo: false +matrix: + include: + - python: "2.7" + env: TOX_ENV=py27 + - python: "3.3" + env: TOX_ENV=py33 + - python: "3.4" + env: TOX_ENV=py34 + - 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 --use-mirrors + - pip install tox - pip install coveralls script: - tox -e $TOX_ENV diff --git a/CHANGELOG b/CHANGELOG index e5e1b8adf..346c62ef9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,25 @@ +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 + + * 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 @@ -38,7 +60,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 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/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/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/core.py b/SoftLayer/CLI/core.py index 066b2173e..2830865f1 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,8 +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']}) -@click.pass_context + context_settings={'help_option_names': ['-h', '--help'], + 'auto_envvar_prefix': 'SLCLI'}) @click.option('--format', default=DEFAULT_FORMAT, help="Output format", @@ -84,7 +84,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()]))) @@ -99,17 +99,18 @@ 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, 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, @@ -121,9 +122,8 @@ def cli(ctx, """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() @@ -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) @@ -173,12 +171,12 @@ def output_result(ctx, 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," @@ -194,6 +192,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/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/environment.py b/SoftLayer/CLI/environment.py index ab3d1dbe0..9298a14be 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 @@ -29,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) @@ -47,9 +49,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.""" @@ -68,7 +70,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 +82,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.""" @@ -93,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. @@ -119,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/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/CLI/firewall/add.py b/SoftLayer/CLI/firewall/add.py index 9bf4db177..0e7b8bebc 100644 --- a/SoftLayer/CLI/firewall/add.py +++ b/SoftLayer/CLI/firewall/add.py @@ -15,12 +15,15 @@ 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 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) diff --git a/SoftLayer/CLI/firewall/cancel.py b/SoftLayer/CLI/firewall/cancel.py index 2953ecff2..2816f1323 100644 --- a/SoftLayer/CLI/firewall/cancel.py +++ b/SoftLayer/CLI/firewall/cancel.py @@ -1,4 +1,4 @@ -"""List firewalls.""" +"""Cancels a firewall.""" # :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/formatting.py b/SoftLayer/CLI/formatting.py index 6f35a9855..92cc2cc3e 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'] @@ -262,7 +263,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 @@ -338,3 +343,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 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/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/server/detail.py b/SoftLayer/CLI/server/detail.py index 943b694be..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']) @@ -96,8 +99,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/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/CLI/server/list.py b/SoftLayer/CLI/server/list.py index d6f456221..9ca77093b 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(['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') @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('--columns', help='Columns to display. default is ' + ' 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): +def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, + columns): """List hardware servers.""" manager = SoftLayer.HardwareManager(env.client) + columns_clean = [col.strip() for col in columns.split(',')] servers = manager.list_hardware(hostname=hostname, domain=domain, @@ -40,25 +40,31 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag): nic_speed=network, tags=tag) - table = formatting.Table([ - 'id', - 'hostname', - 'primary_ip', - 'backend_ip', - 'datacenter', - 'action', - ]) - table.sortby = sortby or 'hostname' + table = formatting.Table(columns_clean) + 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' + column_map['powerState'] = 'powerState-name' for server in servers: - table.add_row([ - utils.lookup(server, 'id'), - utils.lookup(server, 'hostname') or formatting.blank(), - utils.lookup(server, 'primaryIpAddress') or formatting.blank(), - utils.lookup(server, 'primaryBackendIpAddress') or - formatting.blank(), - utils.lookup(server, 'datacenter', 'name') or formatting.blank(), - formatting.active_txn(server), - ]) + 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 + 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/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/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, 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/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 3afe151e2..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']: @@ -93,8 +96,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/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') diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 8b92acc1f..1fc934416 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') @@ -28,13 +23,16 @@ @click.option('--tags', help='Show instances that have one of these comma-separated ' 'tags') +@click.option('--columns', help='Columns to display. default is ' + ' 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): + hourly, monthly, tags, columns): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) - + columns_clean = [col.strip() for col in columns.split(',')] tag_list = None if tags: tag_list = [tag.strip() for tag in tags.split(',')] @@ -49,25 +47,31 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, nic_speed=network, tags=tag_list) - table = formatting.Table([ - 'id', - 'hostname', - 'primary_ip', - 'backend_ip', - 'datacenter', - 'action', - ]) - table.sortby = sortby or 'hostname' + table = formatting.Table(columns_clean) + 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' + column_map['powerState'] = 'powerState-name' for guest in guests: - table.add_row([ - utils.lookup(guest, 'id'), - utils.lookup(guest, 'hostname') or formatting.blank(), - utils.lookup(guest, 'primaryIpAddress') or formatting.blank(), - utils.lookup(guest, 'primaryBackendIpAddress') or - formatting.blank(), - utils.lookup(guest, 'datacenter', 'name') or formatting.blank(), - formatting.active_txn(guest), - ]) + 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 + if col in column_map: + entry = guest[column_map[col]] + else: + entry = guest[col] + + row_column.append(entry or formatting.blank()) + + table.add_row(row_column) return table 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.""" diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 0279225c5..d849541fb 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.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/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/managers/hardware.py b/SoftLayer/managers/hardware.py index 70aea1ec1..dc9647a6f 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -26,6 +26,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 @@ -46,6 +56,10 @@ 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. + Example:: + + # Cancels hardware id 1234 + result = mgr.cancel_hardware(hardware_id=1234) """ # Get cancel reason @@ -83,6 +97,12 @@ 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) """ if 'mask' not in kwargs: hw_items = [ @@ -152,6 +172,11 @@ 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 = mgr.get_hardware(hardware_id=1234,mask=object_mask) """ if 'mask' not in kwargs: @@ -219,6 +244,10 @@ 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) """ return self.hardware.bootToRescueLayer(id=hardware_id) @@ -230,6 +259,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 """ if public: func = self.hardware.setPublicNetworkInterfaceSpeed @@ -360,7 +399,7 @@ def _get_package(self): prices ], activePresets, -regions[location[location]] +regions[location[location[priceGroups]]] ''' package_type = 'BARE_METAL_CPU_FAST_PROVISION' @@ -390,25 +429,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 +470,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), @@ -462,7 +508,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,13 +519,23 @@ 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. + + Example:: + # Change the hostname on instance 12345 to 'something' + result = mgr.edit(hardware_id=12345 , hostname="something") + #result will be True or an Exception """ obj = {} 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 @@ -510,6 +566,11 @@ 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) """ return self.hardware.createFirmwareUpdateTransaction( @@ -517,7 +578,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 +586,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 +608,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 +635,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 +659,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 +683,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 +693,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 +722,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/managers/ssl.py b/SoftLayer/managers/ssl.py index 68e78dbdb..9b698015c 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -11,6 +11,18 @@ 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 +36,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 +60,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 +73,10 @@ 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) """ return self.ssl.deleteObject(id=cert_id) @@ -61,6 +88,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 +103,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 30de90b35..0905f6be8 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -23,6 +23,18 @@ 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 +69,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.create_client_from_env() + # Print out a list of 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 +158,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.create_client_from_env() + # 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 """ @@ -196,6 +205,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]', @@ -210,6 +220,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() @@ -218,17 +233,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.create_client_from_env() - - mgr = SoftLayer.VSManager(client) - mgr.cancel_instance(12345) + # Cancels instance 12345 + mgr.cancel_instance(12345) """ return self.guest.deleteObject(id=instance_id) @@ -241,17 +249,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.create_client_from_env() + 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) """ @@ -383,6 +389,11 @@ 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): @@ -422,7 +433,30 @@ 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 """ + kwargs.pop('tags', None) create_options = self._generate_create_dict(**kwargs) return self.guest.generateOrderTemplate(create_options) @@ -458,6 +492,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)) @@ -470,6 +528,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] @@ -490,6 +581,15 @@ 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 """ if public: func = self.guest.setPublicNetworkInterfaceSpeed @@ -534,7 +634,12 @@ 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 """ obj = {} @@ -562,6 +667,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) @@ -576,6 +686,12 @@ 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) """ vsi = self.get_instance(instance_id) @@ -602,12 +718,12 @@ 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 - :: + :returns: bool + Example:: # Upgrade instance 12345 to 4 CPUs and 4 GB of memory import SoftLayer client = SoftLayer.create_client_from_env() - mgr = SoftLayer.VSManager(client) mgr.upgrade(12345, cpus=4, memory=4) 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_env.py b/SoftLayer/shell/cmd_env.py new file mode 100644 index 000000000..e55901baf --- /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 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..eeceef068 --- /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 000000000..40f3c8439 --- /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 000000000..8c5151d2e --- /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 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', +} 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/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/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 0a7fd6fe5..ee2813391 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -19,7 +19,13 @@ 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, + current_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: @@ -93,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: @@ -102,7 +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) - 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 current_path, new_root diff --git a/SoftLayer/tests/CLI/environment_tests.py b/SoftLayer/tests/CLI/environment_tests.py index b3826df89..e4fee1cc9 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' @@ -43,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 873d58a89..b2fb49fa2 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -7,21 +7,18 @@ """ import json import os -import sys +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 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): @@ -373,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', @@ -403,8 +394,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 +409,33 @@ 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.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..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 @@ -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/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..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']) @@ -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']) @@ -92,6 +111,7 @@ def test_create(self, confirm_mock): '--memory=1', '--network=100', '--billing=hourly', + '--datacenter=dal05', '--tag=dev', '--tag=green']) @@ -101,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, 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): diff --git a/SoftLayer/tests/managers/cdn_tests.py b/SoftLayer/tests/managers/cdn_tests.py index d78adb743..e4c20f52d 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 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 efec2591a..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 = { @@ -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)) 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..bfc159531 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): @@ -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', 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/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) diff --git a/docs/conf.py b/docs/conf.py index 8f480496c..00c951ba0 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.1.0' # The full version, including alpha/beta/rc tags. -release = '4.0.4' +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 3f329b77b..fe0c8e954 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): @@ -36,7 +37,7 @@ setup( name='SoftLayer', - version='4.0.4', + version='4.1.0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.', 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 diff --git a/tox.ini b/tox.ini index 2f693b5e8..68c7662f0 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 = @@ -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 \