diff --git a/.travis.yml b/.travis.yml index 8fb11f473..aabd7e374 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,11 @@ language: python -python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "pypy" -# command to install dependencies -install: - - "pip install -r requirements.txt --use-mirrors" - - "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi" -# command to run tests -script: python setup.py nosetests +python: 2.7 +env: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=py33 + - TOX_ENV=pypy + - TOX_ENV=pep8 + - TOX_ENV=pylint +install: pip install tox --use-mirrors +script: tox -e $TOX_ENV diff --git a/CHANGELOG b/CHANGELOG index 8466f1c9e..8b2c3e9bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,26 @@ +3.1.0 + + * CLI+API: Added CDN manager and CLI module + + * CLI+API: Added ticket manager and CLI module + + * CLI+API: Added image manager and improves image CLI module + + * CLI+API: Added the ability to specify a proxy URL for API bindings and the CLI + + * API: six is now used to provide support for Python 2 and Python 3 with the same source + + * CLI+API: Added ability to resize a virtual machine + + * CLI+API: Implemented product name changes in accordance with SoftLayer's new product names. Existing managers should continue to work as before. Minor CLI changes were necessary. + + * CLI+API: Added firewall manager and CLI module + + * CLI+API: Added load balancer manager and CLI module + + * Many bug fixes and minor suggested improvements + + 3.0.2 * CLI+API: Simplified object mask reformatting and added support for more complex masks. diff --git a/LICENSE b/LICENSE index 38a232e2a..e606a227d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013 SoftLayer Technologies, Inc. All rights reserved. +Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index bebe6ad06..4a8ecd9d8 100644 --- a/README.rst +++ b/README.rst @@ -1,24 +1,24 @@ SoftLayer API Python Client =========================== -.. image:: https://api.travis-ci.org/softlayer/softlayer-api-python-client.png - :target: https://travis-ci.org/softlayer/softlayer-api-python-client +.. image:: https://api.travis-ci.org/softlayer/softlayer-python.png + :target: https://travis-ci.org/softlayer/softlayer-python -.. image:: https://landscape.io/github/softlayer/softlayer-api-python-client/master/landscape.png - :target: https://landscape.io/github/softlayer/softlayer-api-python-client/master +.. image:: https://landscape.io/github/softlayer/softlayer-python/master/landscape.png + :target: https://landscape.io/github/softlayer/softlayer-python/master .. image:: https://badge.fury.io/py/SoftLayer.png :target: http://badge.fury.io/py/SoftLayer -.. image:: https://pypip.in/d/SoftLayer/badge.png - :target: https://crate.io/packages/SoftLayer - SoftLayer API bindings for Python. For use with `SoftLayer's API `_. -This library provides a simple interface to interact with SoftLayer's XML-RPC API and provides support for many of SoftLayer API's features like `object masks `_ and a command-line interface that can be used to access various SoftLayer services using the API. +This library provides a simple interface to interact with SoftLayer's XML-RPC API and provides support for many of SoftLayer API's features like `object masks `_ and includes a command-line interface that can be used to manage various SoftLayer services. Documentation ------------- -Documentation is available at https://softlayer-api-python-client.readthedocs.org/ +Documentation is available at https://softlayer-python.readthedocs.org + +* API Client: https://softlayer-python.readthedocs.org/en/latest/api/client.html +* Command-line Interface: https://softlayer-python.readthedocs.org/en/latest/cli.html Installation ------------ @@ -52,5 +52,5 @@ System Requirements Copyright --------- -This software is Copyright (c) 2013 SoftLayer Technologies, Inc. +This software is Copyright (c) 2014 SoftLayer Technologies, Inc. See the bundled LICENSE file for more information. diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 9b7dcc46f..5bd2fdd28 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -3,15 +3,14 @@ ~~~~~~~~~~~~~ SoftLayer API bindings - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: MIT, see LICENSE for more details. """ import time -from consts import API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT, USER_AGENT -from transports import make_xml_rpc_api_call -from auth import TokenAuthentication -from config import get_client_settings +from .consts import API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT, USER_AGENT +from .transports import make_xml_rpc_api_call +from .auth import TokenAuthentication +from .config import get_client_settings __all__ = ['Client', 'TimedClient', 'API_PUBLIC_ENDPOINT', @@ -39,6 +38,7 @@ class Client(object): :param endpoint_url: the API endpoint base URL you wish to connect to. Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. + :param proxy: proxy to be used to make API calls :param integer timeout: timeout for API requests :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. Example: `BasicAuthentication` @@ -56,21 +56,24 @@ class Client(object): _prefix = "SoftLayer_" def __init__(self, username=None, api_key=None, endpoint_url=None, - timeout=None, auth=None, config_file=None): + timeout=None, auth=None, config_file=None, proxy=None): settings = get_client_settings(username=username, api_key=api_key, endpoint_url=endpoint_url, timeout=timeout, auth=auth, + proxy=proxy, config_file=config_file) self.auth = settings.get('auth') self.endpoint_url = ( settings.get('endpoint_url') or API_PUBLIC_ENDPOINT).rstrip('/') self.timeout = None - self.last_calls = [] if settings.get('timeout'): self.timeout = float(settings.get('timeout')) + self.proxy = None + if settings.get('proxy'): + self.proxy = settings.get('proxy') def authenticate_with_password(self, username, password, security_question_id=None, @@ -92,7 +95,7 @@ def authenticate_with_password(self, username, password, security_question_id, security_question_answer) self.auth = TokenAuthentication(res['userId'], res['hash']) - return (res['userId'], res['hash']) + return res['userId'], res['hash'] def __getitem__(self, name): """ Get a SoftLayer Service. @@ -113,8 +116,8 @@ def call(self, service, method, *args, **kwargs): :param service: the name of the SoftLayer API service :param method: the method to call on the service - :param \*args: same optional arguments that ``Service.call`` takes - :param \*\*kwargs: same optional keyword arguments that + :param \\*args: same optional arguments that ``Service.call`` takes + :param \\*\\*kwargs: same optional keyword arguments that ``Service.call`` takes :param service: the name of the SoftLayer API service @@ -137,31 +140,25 @@ def call(self, service, method, *args, **kwargs): if not service.startswith(self._prefix): service = self._prefix + service - objectid = kwargs.get('id') - objectmask = kwargs.get('mask') - objectfilter = kwargs.get('filter') headers = kwargs.get('headers', {}) - compress = kwargs.get('compress', True) - raw_headers = kwargs.get('raw_headers') - limit = kwargs.get('limit') - offset = kwargs.get('offset', 0) if self.auth: headers.update(self.auth.get_headers()) - if objectid is not None: - headers[service + 'InitParameters'] = {'id': objectid} + if kwargs.get('id') is not None: + headers[service + 'InitParameters'] = {'id': kwargs.get('id')} - if objectmask is not None: - headers.update(self.__format_object_mask(objectmask, service)) + if kwargs.get('mask') is not None: + headers.update(self.__format_object_mask(kwargs.get('mask'), + service)) - if objectfilter is not None: - headers['%sObjectFilter' % service] = objectfilter + if kwargs.get('filter') is not None: + headers['%sObjectFilter' % service] = kwargs.get('filter') - if limit: + if kwargs.get('limit'): headers['resultLimit'] = { - 'limit': limit, - 'offset': offset, + 'limit': kwargs.get('limit'), + 'offset': kwargs.get('offset', 0), } http_headers = { @@ -169,18 +166,19 @@ def call(self, service, method, *args, **kwargs): 'Content-Type': 'application/xml', } - if compress: + if kwargs.get('compress', True): http_headers['Accept'] = '*/*' http_headers['Accept-Encoding'] = 'gzip, deflate, compress' - if raw_headers: - http_headers.update(raw_headers) + if kwargs.get('raw_headers'): + http_headers.update(kwargs.get('raw_headers')) uri = '/'.join([self.endpoint_url, service]) return make_xml_rpc_api_call(uri, method, args, headers=headers, http_headers=http_headers, - timeout=self.timeout) + timeout=self.timeout, + proxy=self.proxy) __call__ = call @@ -191,8 +189,8 @@ def iter_call(self, service, method, :param service: the name of the SoftLayer API service :param method: the method to call on the service :param integer chunk: result size for each API call - :param \*args: same optional arguments that ``Service.call`` takes - :param \*\*kwargs: same optional keyword arguments that + :param \\*args: same optional arguments that ``Service.call`` takes + :param \\*\\*kwargs: same optional keyword arguments that ``Service.call`` takes """ @@ -260,6 +258,9 @@ def __repr__(self): __str__ = __repr__ + def __len__(self): + return 0 + class TimedClient(Client): """ Subclass of Client() @@ -268,7 +269,10 @@ class TimedClient(Client): internal list. This will have a slight impact on your client's memory usage and performance. You should only use this for debugging. """ - last_calls = [] + + def __init__(self, *args, **kwargs): + self.last_calls = [] + super(TimedClient, self).__init__(*args, **kwargs) def call(self, service, method, *args, **kwargs): """ See Client.call for documentation. """ @@ -292,6 +296,11 @@ def get_last_calls(self): class Service(object): + """ A SoftLayer Service. + :param client: A SoftLayer.API.Client instance + :param name str: The service name + + """ def __init__(self, client, name): self.client = client self.name = name @@ -300,7 +309,7 @@ def call(self, name, *args, **kwargs): """ Make a SoftLayer API call :param method: the method to call on the service - :param \*args: (optional) arguments for the remote call + :param \\*args: (optional) arguments for the remote call :param id: (optional) id for the resource :param mask: (optional) object mask :param dict filter: (optional) filter dict @@ -328,8 +337,8 @@ def iter_call(self, name, *args, **kwargs): :param method: the method to call on the service :param integer chunk: result size for each API call - :param \*args: same optional arguments that ``Service.call`` takes - :param \*\*kwargs: same optional keyword arguments that + :param \\*args: same optional arguments that ``Service.call`` takes + :param \\*\\*kwargs: same optional keyword arguments that ``Service.call`` takes Usage: @@ -350,6 +359,7 @@ def __getattr__(self, name): raise AttributeError("'Obj' object has no attribute '%s'" % name) def call_handler(*args, **kwargs): + " Handler that actually makes the API call " return self(name, *args, **kwargs) return call_handler diff --git a/SoftLayer/CLI/__init__.py b/SoftLayer/CLI/__init__.py index 821f28710..5e4389c65 100644 --- a/SoftLayer/CLI/__init__.py +++ b/SoftLayer/CLI/__init__.py @@ -3,10 +3,8 @@ ~~~~~~~~~~~~~~ Contains all code related to the CLI interface - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: MIT, see LICENSE for more details. """ +# pylint: disable=w0401 - -import SoftLayer.CLI.core # NOQA from SoftLayer.CLI.helpers import * # NOQA diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index eb9ee50fe..67bcb865b 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -9,14 +9,14 @@ The available modules are: Compute: - bmc Bare Metal Cloud - cci Cloud Compute Instances image Manages compute and flex images metadata Get details about this machine. Also available with 'my' and 'meta' - server Hardware servers + server Bare metal servers sshkey Manage SSH keys on your account + vs Virtual Servers (formerly CCIs) Networking: + cdn Content Delivery Network service management dns Domain Name System firewall Firewall rule and security management globalip Global IP address management @@ -29,9 +29,11 @@ Storage: iscsi View iSCSI details nas View NAS details + snapshot iSCSI snapshots General: config View and edit configuration for this tool + ticket Manage account tickets summary Display an overall summary of your account help Show help @@ -40,7 +42,6 @@ To use most commands your SoftLayer username and api_key need to be configured. The easiest way to do that is to use: 'sl config setup' """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. import sys @@ -50,8 +51,8 @@ from SoftLayer import Client, TimedClient, SoftLayerError, SoftLayerAPIError from SoftLayer.consts import VERSION -from helpers import CLIAbort, ArgumentError, format_output, KeyValueTable -from environment import Environment, InvalidCommand, InvalidModule +from .helpers import CLIAbort, ArgumentError, format_output, KeyValueTable +from .environment import Environment, InvalidCommand, InvalidModule DEBUG_LOGGING_MAP = { @@ -65,18 +66,25 @@ class CommandParser(object): + """ Helper class to parse commands + + :param env: Environment instance + """ def __init__(self, env): self.env = env def get_main_help(self): + """ Get main help text """ return __doc__.strip() def get_module_help(self, module_name): + """ Get help text for a module """ module = self.env.load_module(module_name) arg_doc = module.__doc__ return arg_doc.strip() def get_command_help(self, module_name, command_name): + """ Get help text for a specific command """ command = self.env.get_command(module_name, command_name) default_format = 'raw' @@ -94,16 +102,18 @@ def get_command_help(self, module_name, command_name): if '[options]' in arg_doc: arg_doc += """ Standard Options: - --format=ARG Output format. [Options: table, raw] [Default: %s] - -C FILE --config=FILE Config file location. [Default: ~/.softlayer] - --debug=LEVEL Specifies the debug noise level + --format=ARG Output format. [Options: table, raw] [Default: %s] + -C FILE --config=FILE Config file location. [Default: ~/.softlayer] + --debug=LEVEL Specifies the debug noise level 1=warn, 2=info, 3=debug - --timings Time each API call and display after results - -h --help Show this screen + --timings Time each API call and display after results + --proxy=PROTO:PROXY_URL HTTP[s] proxy to be use to make API calls + -h --help Show this screen """ % default_format return arg_doc.strip() def parse_main_args(self, args): + """ Parse root arguments """ main_help = self.get_main_help() arguments = docopt( main_help, @@ -114,6 +124,7 @@ def parse_main_args(self, args): return arguments def parse_module_args(self, module_name, args): + """ Parse module arguments """ arg_doc = self.get_module_help(module_name) arguments = docopt( arg_doc, @@ -123,12 +134,14 @@ def parse_module_args(self, module_name, args): return arguments def parse_command_args(self, module_name, command_name, args): + """ Parse command arguments """ command = self.env.get_command(module_name, command_name) arg_doc = self.get_command_help(module_name, command_name) arguments = docopt(arg_doc, version=VERSION, argv=[module_name] + args) return command, arguments def parse(self, args): + """ Parse entire tree of arguments """ # handle `sl ...` main_args = self.parse_main_args(args) module_name = main_args[''] @@ -160,14 +173,18 @@ def main(args=sys.argv[1:], env=Environment()): debug_level = command_args.get('--debug') if debug_level: logger = logging.getLogger() - h = logging.StreamHandler() - logger.addHandler(h) + handler = logging.StreamHandler() + logger.addHandler(handler) logger.setLevel(DEBUG_LOGGING_MAP.get(debug_level, logging.DEBUG)) + kwargs = { + 'proxy': command_args.get('--proxy'), + 'config_file': command_args.get('--config') + } if command_args.get('--timings'): - client = TimedClient(config_file=command_args.get('--config')) + client = TimedClient(**kwargs) else: - client = Client(config_file=command_args.get('--config')) + client = Client(**kwargs) # Do the thing runnable = command(client=client, env=env) @@ -176,56 +193,56 @@ def main(args=sys.argv[1:], env=Environment()): out_format = command_args.get('--format', 'table') if out_format not in VALID_FORMATS: raise ArgumentError('Invalid format "%s"' % out_format) - s = format_output(data, fmt=out_format) - if s: - env.out(s) + output = format_output(data, fmt=out_format) + if output: + env.out(output) if command_args.get('--timings'): out_format = command_args.get('--format', 'table') api_calls = client.get_last_calls() - t = KeyValueTable(['call', 'time']) + timing_table = KeyValueTable(['call', 'time']) for call, _, duration in api_calls: - t.add_row([call, duration]) + timing_table.add_row([call, duration]) - env.err(format_output(t, fmt=out_format)) + env.err(format_output(timing_table, fmt=out_format)) - except InvalidCommand as e: - env.err(resolver.get_module_help(e.module_name)) - if e.command_name: + except InvalidCommand as ex: + env.err(resolver.get_module_help(ex.module_name)) + if ex.command_name: env.err('') - env.err(str(e)) + env.err(str(ex)) exit_status = 1 - except InvalidModule as e: + except InvalidModule as ex: env.err(resolver.get_main_help()) - if e.module_name: + if ex.module_name: env.err('') - env.err(str(e)) + env.err(str(ex)) exit_status = 1 - except DocoptExit as e: - env.err(e.usage) + except DocoptExit as ex: + env.err(ex.usage) env.err( '\nUnknown argument(s), use -h or --help for available options') exit_status = 127 except KeyboardInterrupt: env.out('') exit_status = 1 - except CLIAbort as e: - env.err(str(e.message)) - exit_status = e.code - except SystemExit as e: - exit_status = e.code - except SoftLayerAPIError as e: - if 'invalid api token' in e.faultString.lower(): + except CLIAbort as ex: + env.err(str(ex.message)) + exit_status = ex.code + except SystemExit as ex: + exit_status = ex.code + except SoftLayerAPIError as ex: + if 'invalid api token' in ex.faultString.lower(): env.out("Authentication Failed: To update your credentials, use " "'sl config setup'") else: - env.err(str(e)) + env.err(str(ex)) exit_status = 1 - except SoftLayerError as e: - env.err(str(e)) + except SoftLayerError as ex: + env.err(str(ex)) exit_status = 1 - except Exception as e: + except Exception: import traceback env.err(traceback.format_exc()) exit_status = 1 diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index b16877ab4..c6e27f06a 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -3,7 +3,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~ Abstracts everything related to the user's environment when running the CLI - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: MIT, see LICENSE for more details. """ import getpass @@ -14,8 +13,11 @@ import sys from SoftLayer.CLI.modules import get_module_list +from SoftLayer.utils import console_input from SoftLayer import SoftLayerError +# pylint: disable=R0201 + class InvalidCommand(SoftLayerError): " Raised when trying to use a command that does not exist " @@ -35,20 +37,26 @@ def __init__(self, module_name, *args): class Environment(object): - # {'module_name': {'action': 'actionClass'}} - plugins = {} - aliases = { - 'meta': 'metadata', - 'my': 'metadata', - 'vm': 'cci', - 'hardware': 'server', - 'hw': 'server', - 'bmetal': 'bmc', - } - stdout = sys.stdout - stderr = sys.stderr + """ Provides access to the current CLI environment """ + def __init__(self): + # {'module_name': {'action': 'actionClass'}} + self.plugins = {} + self.aliases = { + 'meta': 'metadata', + 'my': 'metadata', + 'vm': 'vs', + 'cci': 'vs', + 'hardware': 'server', + 'hw': 'server', + 'bmetal': 'bmc', + 'virtual': 'vs', + 'lb': 'loadbal', + } + self.stdout = sys.stdout + self.stderr = sys.stderr def get_command(self, module_name, command_name): + """ Based on the loaded modules, return a command """ actions = self.plugins.get(module_name) or {} if command_name in actions: return actions[command_name] @@ -57,11 +65,13 @@ def get_command(self, module_name, command_name): raise InvalidCommand(module_name, command_name) def get_module_name(self, module_name): + """ Returns the actual module name. Uses the alias mapping """ if module_name in self.aliases: return self.aliases[module_name] return module_name def load_module(self, module_name): # pragma: no cover + """ Loads module by name """ try: module = import_module('SoftLayer.CLI.modules.%s' % module_name) for _, obj in inspect.getmembers(module): @@ -72,41 +82,52 @@ def load_module(self, module_name): # pragma: no cover raise InvalidModule(module_name) def add_plugin(self, cls): + """ Add a CLIRunnable as a plugin to the environment """ command = cls.__module__.split('.')[-1] if command not in self.plugins: self.plugins[command] = {} self.plugins[command][cls.action] = cls def plugin_list(self): + """ Returns the list of modules in SoftLayer.CLI.modules """ return get_module_list() - def out(self, s, nl=True): - self.stdout.write(s) - if nl: + def out(self, output, newline=True): + """ Outputs a string to the console (stdout) """ + self.stdout.write(output) + if newline: self.stdout.write(os.linesep) - def err(self, s, nl=True): - self.stderr.write(s) - if nl: + def err(self, output, newline=True): + """ Outputs an error string to the console (stderr) """ + self.stderr.write(output) + if newline: self.stderr.write(os.linesep) def input(self, prompt): - return raw_input(prompt) + """ Provide a command prompt """ + return console_input(prompt) def getpass(self, prompt): + """ Provide a password prompt """ return getpass.getpass(prompt) def exit(self, code=0): + """ Exit """ sys.exit(code) class CLIRunnable(object): + """ CLIRunnable is intended to be subclassed. It represents a descrete + command or action in the CLI. """ options = [] # set by subclass - action = None # set by subclass + action = 'not set' # set by subclass def __init__(self, client=None, env=None): self.client = client self.env = env def execute(self, args): + """ Execute the command. This is intended to be overridden in a + subclass """ pass diff --git a/SoftLayer/CLI/exceptions.py b/SoftLayer/CLI/exceptions.py index 104ae90f3..100966945 100644 --- a/SoftLayer/CLI/exceptions.py +++ b/SoftLayer/CLI/exceptions.py @@ -3,24 +3,26 @@ ~~~~~~~~~~~~~~~~~~~~~~~~ Exceptions to be used in the CLI modules. - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: MIT, see LICENSE for more details. """ class CLIHalt(SystemExit): + """ Smoothly halt the execution of the command. No error """ def __init__(self, code=0, *args): super(CLIHalt, self).__init__(*args) self.code = code class CLIAbort(CLIHalt): + """ Halt the execution of the command. Gives an exit code of 2 """ def __init__(self, msg, *args): super(CLIAbort, self).__init__(code=2, *args) self.message = msg class ArgumentError(CLIAbort): + """ Halt the execution of the command because of invalid arguments. """ def __init__(self, msg, *args): super(ArgumentError, self).__init__(msg, *args) self.message = "Argument Error: %s" % msg diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index 69f008496..4ab878d8c 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -4,23 +4,25 @@ Provider classes and helper functions to display output onto a command-line. - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: MIT, see LICENSE for more details. """ +# pylint: disable=E0202 import os import json from prettytable import PrettyTable, FRAME, NONE +from SoftLayer.utils import string_types, console_input -def format_output(data, fmt='table'): + +def format_output(data, fmt='table'): # pylint: disable=R0911,R0912 """ Given some data, will format it for output :param data: One of: String, Table, FormattedItem, List, Tuple, SequentialOutput :param string fmt (optional): One of: table, raw, json, python """ - if isinstance(data, basestring): + if isinstance(data, string_types): if fmt == 'json': return json.dumps(data) return data @@ -64,30 +66,34 @@ def format_output(data, fmt='table'): def format_prettytable(table): + """ Takes a SoftLayer.CLI.formatting.Table instance and returns a formatted + prettytable """ for i, row in enumerate(table.rows): for j, item in enumerate(row): table.rows[i][j] = format_output(item) - t = table.prettytable() - t.hrules = FRAME - t.horizontal_char = '.' - t.vertical_char = ':' - t.junction_char = ':' - return t + ptable = table.prettytable() + ptable.hrules = FRAME + ptable.horizontal_char = '.' + ptable.vertical_char = ':' + ptable.junction_char = ':' + return ptable def format_no_tty(table): + """ Takes a SoftLayer.CLI.formatting.Table instance and returns a formatted + prettytable that has as little formatting as possible """ for i, row in enumerate(table.rows): for j, item in enumerate(row): table.rows[i][j] = format_output(item, fmt='raw') - t = table.prettytable() + ptable = table.prettytable() for col in table.columns: - t.align[col] = 'l' - t.hrules = NONE - t.border = False - t.header = False - t.left_padding_width = 0 - t.right_padding_width = 2 - return t + ptable.align[col] = 'l' + ptable.hrules = NONE + ptable.border = False + ptable.header = False + ptable.left_padding_width = 0 + ptable.right_padding_width = 2 + return ptable def mb_to_gb(megabytes): @@ -99,7 +105,7 @@ def mb_to_gb(megabytes): return FormattedItem(megabytes, "%dG" % (float(megabytes) / 1024)) -def gb(gigabytes): +def gb(gigabytes): # pylint: disable=C0103 """ Takes in the number of gigabytes and returns a FormattedItem that displays gigabytes. @@ -133,7 +139,7 @@ def active_txn(item): :param item: An object capable of having an active transaction """ - if not item['activeTransaction']['transactionStatus']: + if not item['activeTransaction'].get('transactionStatus'): return blank() return FormattedItem( @@ -141,8 +147,30 @@ def active_txn(item): item['activeTransaction']['transactionStatus'].get('friendlyName')) +def transaction_status(transaction): + """ Returns a FormattedItem describing the transaction status (if any) on + the given object. If there is no status, returns a blank FormattedItem. + + :param item: An object capable of having an active transaction + """ + if not transaction.get('transactionStatus'): + return blank() + + return FormattedItem( + transaction['transactionStatus'].get('name'), + transaction['transactionStatus'].get('friendlyName')) + + def valid_response(prompt, *valid): - ans = raw_input(prompt).lower() + """ Will display a prompt for a command-line user. If the input is in the + valid given valid list then it will return True. Otherwise, it will + return False. If no input is received from the user, None is returned + instead. + + :param string prompt: string prompt to give to the user + :param string \\*valid: valid responses + """ + ans = console_input(prompt).lower() if ans in valid: return True @@ -153,6 +181,11 @@ def valid_response(prompt, *valid): def confirm(prompt_str, default=False): + """ Show a confirmation prompt to a command-line user. + + :param string prompt_str: prompt to give to the user + :param bool default: Default value to True or False + """ if default: prompt = '%s [Y/n]: ' % prompt_str else: @@ -167,6 +200,11 @@ def confirm(prompt_str, default=False): def no_going_back(confirmation): + """ Show a confirmation to a user. + + :param confirmation str: the string the user has to enter in order to + confirm their action. + """ if not confirmation: confirmation = 'yes' @@ -177,11 +215,17 @@ def no_going_back(confirmation): class SequentialOutput(list): + """ This object represents output in a sequence. The purpose is to + de-couple the separator from the output itself. + + :param separator str: string to use as a default separator + """ def __init__(self, separator=os.linesep, *args, **kwargs): self.separator = separator super(SequentialOutput, self).__init__(*args, **kwargs) def to_python(self): + """ returns itself, since it itself is a list """ return self def __str__(self): @@ -189,13 +233,21 @@ def __str__(self): class CLIJSONEncoder(json.JSONEncoder): + " A JSON encoder which is able to use a .to_python() method on objects. " def default(self, obj): + """ If the normal JSONEncoder doesn't understand, we have a chance to + encode the object into a native python type. + """ if hasattr(obj, 'to_python'): return obj.to_python() return super(CLIJSONEncoder, self).default(obj) class Table(object): + """ A Table structure. + + :param list columns: a list of column names + """ def __init__(self, columns): self.columns = columns self.rows = [] @@ -204,44 +256,53 @@ def __init__(self, columns): self.sortby = None def add_row(self, row): - self.rows.append(row) + """ Add a row to the table - def _format_python_value(self, value): - if hasattr(value, 'to_python'): - return value.to_python() - return value + :param list row: the row of string to be added + """ + self.rows.append(row) def to_python(self): + """ Decode this Table object to standard Python types """ # Adding rows - l = [] + items = [] for row in self.rows: - formatted_row = [self._format_python_value(v) for v in row] - l.append(dict(zip(self.columns, formatted_row))) - return l + formatted_row = [_format_python_value(v) for v in row] + items.append(dict(zip(self.columns, formatted_row))) + return items def prettytable(self): """ Returns a new prettytable instance. """ - t = PrettyTable(self.columns) + table = PrettyTable(self.columns) if self.sortby: - t.sortby = self.sortby + table.sortby = self.sortby for a_col, alignment in self.align.items(): - t.align[a_col] = alignment + table.align[a_col] = alignment # Adding rows for row in self.rows: - t.add_row(row) - return t + table.add_row(row) + return table class KeyValueTable(Table): + """ This is a Table which is intended to be used to display key-value + pairs. It expects there to be only two columns.""" def to_python(self): - d = {} + """ Decode this KeyValueTable object to standard Python types """ + mapping = {} for row in self.rows: - d[row[0]] = self._format_python_value(row[1]) - return d + mapping[row[0]] = _format_python_value(row[1]) + return mapping class FormattedItem(object): + """ This is an object which wraps a single value that is able to be + represented in a human readable and a machine-readable way. + + :param original: raw (machine-readable) value + :param string formatted: human-readable value + """ def __init__(self, original, formatted=None): self.original = original if formatted is not None: @@ -250,11 +311,21 @@ def __init__(self, original, formatted=None): self.formatted = self.original def to_python(self): + """ returns the original (raw) value """ return self.original def __str__(self): + """ returns the formatted value """ + # If the original value is None, represent this as 'NULL' if self.original is None: return 'NULL' return str(self.original) __repr__ = __str__ + + +def _format_python_value(value): + """ If the value has to_python() defined then return that """ + if hasattr(value, 'to_python'): + return value.to_python() + return value diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index 20bb1dae5..6c9e928ad 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -3,18 +3,17 @@ ~~~~~~~~~~~~~~~~~~~~~ Helpers to be used in CLI modules in SoftLayer.CLI.modules.* - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: MIT, see LICENSE for more details. """ from SoftLayer.utils import NestedDict from SoftLayer.CLI.environment import CLIRunnable -from exceptions import CLIHalt, CLIAbort, ArgumentError -from formatting import ( +from .exceptions import CLIHalt, CLIAbort, ArgumentError +from .formatting import ( Table, KeyValueTable, FormattedItem, SequentialOutput, confirm, no_going_back, mb_to_gb, gb, listing, blank, format_output, - active_txn, valid_response) -from template import update_with_template_args, export_to_template + active_txn, valid_response, transaction_status) +from .template import update_with_template_args, export_to_template __all__ = [ # Core/Misc @@ -24,7 +23,7 @@ # Formatting 'Table', 'KeyValueTable', 'FormattedItem', 'SequentialOutput', 'valid_response', 'confirm', 'no_going_back', 'mb_to_gb', 'gb', - 'listing', 'format_output', 'blank', 'active_txn', + 'listing', 'format_output', 'blank', 'active_txn', 'transaction_status', # Template 'update_with_template_args', 'export_to_template', ] diff --git a/SoftLayer/CLI/modules/__init__.py b/SoftLayer/CLI/modules/__init__.py index cecc50989..99add6466 100644 --- a/SoftLayer/CLI/modules/__init__.py +++ b/SoftLayer/CLI/modules/__init__.py @@ -3,7 +3,6 @@ ~~~~~~~~~~~~~~~~~~~~~ Contains all plugable modules for the CLI interface - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: MIT, see LICENSE for more details. """ @@ -11,5 +10,6 @@ def get_module_list(): + """ Returns each module under SoftLayer.CLI.modules """ actions = [action[1] for action in iter_modules(__path__)] return actions diff --git a/SoftLayer/CLI/modules/bmc.py b/SoftLayer/CLI/modules/bmc.py deleted file mode 100644 index 7dd2195f6..000000000 --- a/SoftLayer/CLI/modules/bmc.py +++ /dev/null @@ -1,527 +0,0 @@ -""" -usage: sl bmc [] [...] [options] - sl bmc [-h | --help] - -Manage bare metal instances - -The available commands are: - cancel Cancels a bare metal instance - create Create a new bare metal instance - create-options Output available available options when creating a server - -For several commands, will be asked for. This can be the id, -hostname or the ip address for a piece of hardware. -""" -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. -# :license: MIT, see LICENSE for more details. -import re -from os import linesep -from SoftLayer.CLI import ( - CLIRunnable, Table, KeyValueTable, no_going_back, confirm, listing, - FormattedItem) -from SoftLayer.CLI.helpers import ( - ArgumentError, CLIAbort, SequentialOutput, update_with_template_args, - FALSE_VALUES, resolve_id) -from SoftLayer import HardwareManager, SshKeyManager - - -class BMCCreateOptions(CLIRunnable): - """ -usage: sl bmc create-options [options] - -Output available available options when creating a bare metal instance. - -Options: - --all Show all options. default if no other option provided - --cpu Show CPU options - --datacenter Show datacenter options - --disk Show disk options - --memory Show memory size options - --nic Show NIC speed options - --os Show operating system options -""" - action = 'create-options' - options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic'] - - def execute(self, args): - t = KeyValueTable(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' - - show_all = True - for opt_name in self.options: - if args.get("--" + opt_name): - show_all = False - break - - mgr = HardwareManager(self.client) - - bmi_options = mgr.get_bare_metal_create_options() - - if args['--all']: - show_all = True - - if args['--datacenter'] or show_all: - results = self.get_create_options(bmi_options, 'datacenter')[0] - - t.add_row([results[0], listing(sorted(results[1]))]) - - if args['--cpu'] or args['--memory'] or show_all: - results = self.get_create_options(bmi_options, 'cpu') - memory_cpu_table = Table(['memory', 'cpu']) - for result in results: - memory_cpu_table.add_row([ - result[0], - listing( - [item[0] for item in sorted( - result[1], key=lambda x: int(x[0]) - )])]) - t.add_row(['memory/cpu', memory_cpu_table]) - - if args['--os'] or show_all: - results = self.get_create_options(bmi_options, 'os') - - for result in results: - t.add_row([ - result[0], - listing( - [item[0] for item in sorted(result[1])], - separator=linesep - )]) - - if args['--disk'] or show_all: - results = self.get_create_options(bmi_options, 'disk')[0] - - t.add_row([results[0], listing( - [item[0] for item in sorted(results[1])])]) - - if args['--nic'] or show_all: - results = self.get_create_options(bmi_options, 'nic') - - for result in results: - t.add_row([result[0], listing( - [item[0] for item in sorted(result[1],)])]) - - return t - - def get_create_options(self, bmi_options, section, pretty=True): - """ This method can be used to parse the bare metal instance creation - options into different sections. This can be useful for data validation - as well as printing the options on a help screen. - - :param dict bmi_options: The instance options to parse. Must come from - the .get_bare_metal_create_options() function - in the HardwareManager. - :param string section: The section to parse out. - :param bool pretty: If true, it will return the results in a 'pretty' - format that's easier to print. - """ - if 'datacenter' == section: - datacenters = [loc['keyname'] - for loc in bmi_options['locations']] - return [('datacenter', datacenters)] - elif 'cpu' == section or 'memory' == section: - mem_options = {} - cpu_regex = re.compile('(\d+) x ') - memory_regex = re.compile(' - (\d+) GB Ram', re.I) - - for item in bmi_options['categories']['server_core']['items']: - cpu = cpu_regex.search(item['description']).group(1) - memory = memory_regex.search(item['description']).group(1) - - if cpu and memory: - if memory not in mem_options: - mem_options[memory] = [] - - mem_options[memory].append((cpu, item['price_id'])) - - results = [] - for memory in sorted(mem_options.keys(), key=int): - key = memory - - if pretty: - key = memory - - results.append((key, mem_options[memory])) - - return results - elif 'os' == section: - os_regex = re.compile('(^[A-Za-z\s\/]+) ([\d\.]+)') - bit_regex = re.compile(' \((\d+)\s*bit') - extra_regex = re.compile(' - (.+)\(') - - # Encapsulate the code for generating the operating system code - def _generate_os_code(name, version, bits, extra_info): - name = name.replace(' Linux', '') - name = name.replace('Enterprise', '') - name = name.replace('GNU/Linux', '') - - os_code = name.strip().replace(' ', '_').upper() - - if os_code == 'RED_HAT': - os_code = 'REDHAT' - - if 'UBUNTU' in os_code: - version = re.sub('\.\d+', '', version) - - os_code += '_' + version.replace('.0', '') - - if bits: - os_code += '_' + bits - - if extra_info: - os_code += '_' + extra_info.strip() \ - .replace(' Install', '').upper() - - return os_code - - # Also separate out the code for generating the Windows OS code - # since it's significantly different from the rest. - def _generate_windows_code(description): - version_check = re.search('Windows Server (\d+)', description) - version = version_check.group(1) - - os_code = 'WIN_' + version - - if 'Datacenter' in description: - os_code += '-DC' - elif 'Enterprise' in description: - os_code += '-ENT' - else: - os_code += '-STD' - - if 'ith R2' in description: - os_code += '-R2' - elif 'ith Hyper-V' in description: - os_code += '-HYPERV' - - bit_check = re.search('\((\d+)\s*bit', description) - if bit_check: - os_code += '_' + bit_check.group(1) - - return os_code - - # Loop through the operating systems and get their OS codes - os_list = {} - flat_list = [] - - for os in bmi_options['categories']['os']['items']: - if 'Windows Server' in os['description']: - os_code = _generate_windows_code(os['description']) - else: - os_results = os_regex.search(os['description']) - name = os_results.group(1) - version = os_results.group(2) - bits = bit_regex.search(os['description']) - extra_info = extra_regex.search(os['description']) - - if bits: - bits = bits.group(1) - if extra_info: - extra_info = extra_info.group(1) - - os_code = _generate_os_code(name, version, bits, - extra_info) - - name = os_code.split('_')[0] - - if name not in os_list: - os_list[name] = [] - - os_list[name].append((os_code, os['price_id'])) - flat_list.append((os_code, os['price_id'])) - - if pretty: - results = [] - for os in sorted(os_list.keys()): - results.append(('os (%s)' % os, os_list[os])) - - return results - else: - return [('os', flat_list)] - - elif 'disk' == section: - disks = [] - for disk in bmi_options['categories']['disk0']['items']: - disks.append((int(disk['capacity']), disk['price_id'])) - - return [('disks', disks)] - elif 'nic' == section: - single = [] - dual = [] - - for item in bmi_options['categories']['port_speed']['items']: - if 'dual' in item['description'].lower(): - dual.append((str(int(item['capacity'])) + '_DUAL', - item['price_id'])) - else: - single.append((str(int(item['capacity'])), - item['price_id'])) - - return [('single nic', single), ('dual nic', dual)] - - return [] - - -class CreateBMCInstance(CLIRunnable): - """ -usage: sl bmc create [--disk=SIZE...] [--key=KEY...] [options] - -Order/create a bare metal instance. See 'sl bmc create-options' for valid -options - -NOTE: Due to hardware configurations, the CPU and memory must match - appropriately. See create-options for options - -Required: - -c --cpu=CPU Number of CPU cores - -D --domain=DOMAIN Domain portion of the FQDN example: example.com - -H --hostname=HOST Host portion of the FQDN. example: server - -m --memory=MEMORY Memory in mebibytes. Example: 2048 - -o OS, --os=OS OS install code - --hourly Hourly rate instance type - --monthly Monthly rate instance type - - -Optional: - -d DC, --datacenter=DC datacenter name Note: Omitting this value defaults - to the first available datacenter - --dry-run, --test Do not create the instance, just get a quote - --export=FILE Exports options to a template file - -d, --disk=SIZE... Disks. Can be specified multiple times - -k KEY, --key=KEY SSH keys to assign to the root user. Can be - specified multiple times. - -n MBPS, --network=MBPS Network port speed in Mbps - --vlan_public=VLAN The ID of the public VLAN on which you want the - hardware placed. - --vlan_private=VLAN The ID of the private VLAN on which you want the - hardware placed. - -t, --template=FILE A template file that defaults the command-line - options using the long name in INI format - -""" - action = 'create' - options = ['confirm'] - required_params = ['--hostname', '--domain', '--cpu', '--memory', '--os'] - - def execute(self, args): - update_with_template_args(args) - mgr = HardwareManager(self.client) - - # Disks will be a comma-separated list. Let's make it a real list. - if isinstance(args.get('--disk'), str): - args['--disk'] = args.get('--disk').split(',') - - # Do the same thing for SSH keys - if isinstance(args.get('--key'), str): - args['--key'] = args.get('--key').split(',') - - self._validate_args(args) - - bmi_options = mgr.get_bare_metal_create_options() - - order = { - 'hostname': args['--hostname'], - 'domain': args['--domain'], - 'bare_metal': True, - } - - # Validate the CPU/Memory combination and get the price ID - server_core = self._get_cpu_and_memory_price_ids(bmi_options, - args['--cpu'], - args['--memory']) - - if server_core: - order['server'] = server_core - else: - raise CLIAbort('Invalid CPU/memory combination specified.') - - order['hourly'] = args['--hourly'] - - # Convert the OS code back into a price ID - os_price = self._get_price_id_from_options(bmi_options, 'os', - args['--os']) - - if os_price: - order['os'] = os_price - else: - raise CLIAbort('Invalid operating system specified.') - - order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' - - # Set the disk size - disk_prices = [] - for disk in args.get('--disk'): - disk_price = self._get_price_id_from_options(bmi_options, 'disk', - disk) - - if disk_price: - disk_prices.append(disk_price) - - if not disk_prices: - disk_prices.append(self._get_default_value(bmi_options, 'disk0')) - - order['disks'] = disk_prices - - # Set the port speed - port_speed = args.get('--network') or 10 - - nic_price = self._get_price_id_from_options(bmi_options, 'nic', - port_speed) - - if nic_price: - order['port_speed'] = nic_price - else: - raise CLIAbort('Invalid NIC speed specified.') - - # Get the SSH keys - if args.get('--key'): - keys = [] - for key in args.get('--key'): - key_id = resolve_id(SshKeyManager(self.client).resolve_ids, - key, 'SshKey') - keys.append(key_id) - order['ssh_keys'] = keys - - if args.get('--vlan_public'): - order['public_vlan'] = args['--vlan_public'] - - if args.get('--vlan_private'): - order['private_vlan'] = args['--vlan_private'] - - # Begin output - t = Table(['Item', 'cost']) - t.align['Item'] = 'r' - t.align['cost'] = 'r' - - if args.get('--test'): - result = mgr.verify_order(**order) - - total_monthly = 0.0 - total_hourly = 0.0 - for price in result['prices']: - total_monthly += float(price.get('recurringFee', 0.0)) - total_hourly += float(price.get('hourlyRecurringFee', 0.0)) - if args.get('--hourly'): - rate = "%.2f" % float(price['hourlyRecurringFee']) - else: - rate = "%.2f" % float(price['recurringFee']) - - t.add_row([price['item']['description'], rate]) - - if args.get('--hourly'): - total = total_hourly - else: - total = total_monthly - - billing_rate = 'monthly' - if args.get('--hourly'): - billing_rate = 'hourly' - t.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) - output = SequentialOutput() - output.append(t) - output.append(FormattedItem( - '', - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guaranteed.') - ) - elif args['--really'] or confirm( - "This action will incur charges on your account. Continue?"): - result = mgr.place_order(**order) - - t = KeyValueTable(['name', 'value']) - t.align['name'] = 'r' - t.align['value'] = 'l' - t.add_row(['id', result['orderId']]) - t.add_row(['created', result['orderDate']]) - output = t - else: - raise CLIAbort('Aborting bare metal instance order.') - - return output - - def _validate_args(self, args): - invalid_args = [k for k in self.required_params if args.get(k) is None] - if invalid_args: - raise ArgumentError('Missing required options: %s' - % ','.join(invalid_args)) - - if args['--hourly'] in FALSE_VALUES: - args['--hourly'] = False - - if args['--monthly'] in FALSE_VALUES: - args['--monthly'] = False - - if all([args['--hourly'], args['--monthly']]): - raise ArgumentError('[--hourly] not allowed with [--monthly]') - - if not any([args['--hourly'], args['--monthly']]): - raise ArgumentError('One of [--hourly | --monthly] is required') - - def _get_cpu_and_memory_price_ids(self, bmi_options, cpu_value, - memory_value): - bmi_obj = BMCCreateOptions() - price_id = None - - cpu_regex = re.compile('(\d+)') - for k, v in bmi_obj.get_create_options(bmi_options, 'cpu'): - cpu = cpu_regex.search(k).group(1) - - if cpu == cpu_value: - for mem_options in v: - if mem_options[0] == memory_value: - price_id = mem_options[1] - - return price_id - - def _get_default_value(self, bmi_options, option): - if option not in bmi_options['categories']: - return - - for item in bmi_options['categories'][option]['items']: - if not any([ - float(item.get('setupFee', 0)), - float(item.get('recurringFee', 0)), - float(item.get('hourlyRecurringFee', 0)), - float(item.get('oneTimeFee', 0)), - float(item.get('laborFee', 0)), - ]): - return item['price_id'] - - def _get_price_id_from_options(self, bmi_options, option, value): - bmi_obj = BMCCreateOptions() - price_id = None - - for _, v in bmi_obj.get_create_options(bmi_options, option, False): - for item_options in v: - if item_options[0] == value: - price_id = item_options[1] - - return price_id - - -class CancelInstance(CLIRunnable): - """ -usage: sl bmc cancel [options] - -Cancel a bare metal instance - -Options: - --immediate Cancels the instance immediately (instead of on the billing - anniversary) -""" - - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - hw = HardwareManager(self.client) - hw_id = resolve_id( - hw.resolve_ids, args.get(''), 'hardware') - - immediate = args.get('--immediate', False) - - if args['--really'] or no_going_back(hw_id): - hw.cancel_metal(hw_id, immediate) - else: - CLIAbort('Aborted') diff --git a/SoftLayer/CLI/modules/cdn.py b/SoftLayer/CLI/modules/cdn.py new file mode 100644 index 000000000..839794b25 --- /dev/null +++ b/SoftLayer/CLI/modules/cdn.py @@ -0,0 +1,186 @@ +""" +usage: sl cdn [] [...] [options] + +Manage CDN accounts and configuration + +The available commands are: + detail Show details for a CDN account + list List CDN accounts + load Cache one or more files on all edge nodes + origin-add Add an origin pull mapping + origin-list Show origin pull mappings on a CDN account + origin-remove Remove an origin pull mapping + purge Purge one or more cached files from all edge nodes +""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer.CLI import CLIRunnable, Table, KeyValueTable, blank +from SoftLayer.managers.cdn import CDNManager + + +class ListAccounts(CLIRunnable): + """ +usage: sl cdn list [options] + +List all CDN accounts + +Options: + --sortby=SORTBY Sort by this value. [Default: id] + [Options: id, account_name, type, created, notes] +""" + action = 'list' + + def execute(self, args): + manager = CDNManager(self.client) + accounts = manager.list_accounts() + + table = Table(['id', 'account_name', 'type', 'created', 'notes']) + for account in accounts: + table.add_row([ + account['id'], + account['cdnAccountName'], + account['cdnSolutionName'], + account['createDate'], + account.get('cdnAccountNote', blank()) + ]) + + table.sortby = args['--sortby'] + return table + + +class DetailAccount(CLIRunnable): + """ +usage: sl cdn detail [options] + +Show CDN account details +""" + action = 'detail' + + def execute(self, args): + manager = CDNManager(self.client) + account = manager.get_account(args.get('')) + + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + table.add_row(['id', account['id']]) + table.add_row(['account_name', account['cdnAccountName']]) + table.add_row(['type', account['cdnSolutionName']]) + table.add_row(['status', account['status']['name']]) + table.add_row(['created', account['createDate']]) + table.add_row(['notes', account.get('cdnAccountNote', blank())]) + + return table + + +class LoadContent(CLIRunnable): + """ +usage: sl cdn load ... [options] + +Cache one or more files on all edge nodes + +Required: + account The CDN account ID to cache content in + content_url The CDN URL(s) or CDN CNAME-based URL(s) for the content + you wish to cache (can be repeated) +""" + action = 'load' + required_params = ['account', 'content_url'] + + def execute(self, args): + manager = CDNManager(self.client) + return str(manager.load_content(args.get(''), + args.get(''))) + + +class PurgeContent(CLIRunnable): + """ +usage: sl cdn purge ... [options] + +Purge one or more cached files from all edge nodes + +Required: + account The CDN account ID to purge content from + content_url The CDN URL(s) or CDN CNAME-based URL(s) for the content + you wish to cache (can be repeated) +""" + action = 'purge' + required_params = ['account', 'content_url'] + + def execute(self, args): + manager = CDNManager(self.client) + manager.purge_content(args.get(''), + args.get('')) + + +class ListOrigins(CLIRunnable): + """ +usage: sl cdn origin-list [options] + +List origin pull mappings associated with a CDN account. +""" + action = 'origin-list' + + def execute(self, args): + manager = CDNManager(self.client) + origins = manager.get_origins(args.get('')) + + table = Table(['id', 'media_type', 'cname', 'origin_url']) + + for origin in origins: + table.add_row([origin['id'], + origin['mediaType'], + origin.get('cname', blank()), + origin['originUrl']]) + + return table + + +class AddOrigin(CLIRunnable): + """ +usage: sl cdn origin-add [options] + +Create an origin pull mapping on a CDN account + +Required: + account The CDN account ID to create a mapping on + url A full URL where content should be pulled from by + CDN edge nodes + +Options: + --type=TYPE The media type for this mapping (http, flash, wm, ...) + (default: http) + --cname=CNAME An optional CNAME to attach to the mapping +""" + action = 'origin-add' + required_params = ['account', 'url'] + + def execute(self, args): + manager = CDNManager(self.client) + media_type = args.get('--type', 'http') + + if not media_type: + media_type = 'http' + + manager.add_origin(args.get(''), media_type, + args.get(''), args.get('--cname', None)) + + +class RemoveOrigin(CLIRunnable): + """ +usage: sl cdn origin-remove [options] + +Remove an origin pull mapping from a CDN account + +Required: + account The CDN account ID to remove a mapping from + origin_id The origin mapping ID to remove +""" + action = 'origin-remove' + required_params = ['account', 'origin_id'] + + def execute(self, args): + manager = CDNManager(self.client) + manager.remove_origin(args.get(''), + args.get('')) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index c40d24d8f..37f111b5c 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -7,7 +7,6 @@ setup Setup configuration show Show current configuration """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. import os.path @@ -16,7 +15,7 @@ Client, SoftLayerAPIError, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) from SoftLayer.CLI import ( CLIRunnable, CLIAbort, KeyValueTable, confirm, format_output) -import ConfigParser +from SoftLayer.utils import configparser def get_settings_from_client(client): @@ -40,17 +39,20 @@ def get_settings_from_client(client): def config_table(settings): - t = KeyValueTable(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' - t.add_row(['Username', settings['username'] or 'not set']) - t.add_row(['API Key', settings['api_key'] or 'not set']) - t.add_row(['Endpoint URL', settings['endpoint_url'] or 'not set']) - t.add_row(['Timeout', settings['timeout'] or 'not set']) - return t + """ Returns a config table """ + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + table.add_row(['Username', settings['username'] or 'not set']) + table.add_row(['API Key', settings['api_key'] or 'not set']) + table.add_row(['Endpoint URL', settings['endpoint_url'] or 'not set']) + table.add_row(['Timeout', settings['timeout'] or 'not set']) + return table def get_api_key(username, secret, endpoint_url=None): + """ Tries API-Key and password auth to get (and potentially generate) an + API key. """ # Try to use a client with username/api key try: client = Client( @@ -61,8 +63,8 @@ def get_api_key(username, secret, endpoint_url=None): client['Account'].getCurrentUser() return secret - except SoftLayerAPIError as e: - if 'invalid api token' not in e.faultString.lower(): + except SoftLayerAPIError as ex: + if 'invalid api token' not in ex.faultString.lower(): raise # Try to use a client with username/password @@ -145,23 +147,25 @@ def execute(self, args): # Persist the config file. Read the target config file in before # setting the values to avoid clobbering settings - config = ConfigParser.RawConfigParser() + config = configparser.RawConfigParser() config.read(config_path) try: config.add_section('softlayer') - except ConfigParser.DuplicateSectionError: + except configparser.DuplicateSectionError: pass config.set('softlayer', 'username', settings['username']) config.set('softlayer', 'api_key', settings['api_key']) config.set('softlayer', 'endpoint_url', settings['endpoint_url']) - f = os.fdopen(os.open( - config_path, (os.O_WRONLY | os.O_CREAT), 0600), 'w') + config_file = os.fdopen(os.open(config_path, + (os.O_WRONLY | os.O_CREAT), + 0o600), + 'w') try: - config.write(f) + config.write(config_file) finally: - f.close() + config_file.close() return "Configuration Updated Successfully" diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index e4b352d91..72431885b 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -14,7 +14,6 @@ edit Update resource records (bulk/single) remove Remove resource records """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. from SoftLayer.CLI import ( @@ -101,17 +100,19 @@ def execute(self, args): return self.list_all_zones() def list_zone(self, args): + """ list records for a particular zone """ manager = DNSManager(self.client) - t = Table([ + table = Table([ + "id", "record", "type", "ttl", "value", ]) - t.align['ttl'] = 'l' - t.align['record'] = 'r' - t.align['value'] = 'l' + table.align['ttl'] = 'l' + table.align['record'] = 'r' + table.align['value'] = 'l' zone_id = resolve_id(manager.resolve_ids, args[''], name='zone') @@ -126,37 +127,39 @@ def list_zone(self, args): except DNSZoneNotFound: raise CLIAbort("No zone found matching: %s" % args['']) - for rr in records: - t.add_row([ - rr['host'], - rr['type'].upper(), - rr['ttl'], - rr['data'] + for record in records: + table.add_row([ + record['id'], + record['host'], + record['type'].upper(), + record['ttl'], + record['data'] ]) - return t + return table def list_all_zones(self): + """ List all zones """ manager = DNSManager(self.client) zones = manager.list_zones() - t = Table([ + table = Table([ "id", "zone", "serial", "updated", ]) - t.align['serial'] = 'c' - t.align['updated'] = 'c' - - for z in zones: - t.add_row([ - z['id'], - z['name'], - z['serial'], - z['updateDate'], + table.align['serial'] = 'c' + table.align['updated'] = 'c' + + for zone in zones: + table.add_row([ + zone['id'], + zone['name'], + zone['serial'], + zone['updateDate'], ]) - return t + return table class AddRecord(CLIRunnable): @@ -220,12 +223,12 @@ def execute(self, args): except DNSZoneNotFound: raise CLIAbort("No zone found matching: %s" % args['']) - for r in results: - if args['--id'] and r['id'] != args['--id']: + for result in results: + if args['--id'] and str(result['id']) != args['--id']: continue - r['data'] = args['--data'] or r['data'] - r['ttl'] = args['--ttl'] or r['ttl'] - manager.edit_record(r) + result['data'] = args['--data'] or result['data'] + result['ttl'] = args['--ttl'] or result['ttl'] + manager.edit_record(result) class RecordRemove(CLIRunnable): @@ -259,12 +262,12 @@ def execute(self, args): raise CLIAbort("No zone found matching: %s" % args['']) if args['--really'] or no_going_back('yes'): - t = Table(['record']) - for r in records: - if args.get('--id') and args['--id'] != r['id']: + table = Table(['record']) + for result in records: + if args.get('--id') and args['--id'] != result['id']: continue - manager.delete_record(r['id']) - t.add_row([r['id']]) + manager.delete_record(result['id']) + table.add_row([result['id']]) - return t + return table raise CLIAbort("Aborted.") diff --git a/SoftLayer/CLI/modules/filters.py b/SoftLayer/CLI/modules/filters.py index bf5384b4b..ae8073a6d 100644 --- a/SoftLayer/CLI/modules/filters.py +++ b/SoftLayer/CLI/modules/filters.py @@ -22,12 +22,11 @@ Examples: sl server list --datacenter=dal05 sl server list --hostname='prod*' - sl cci list --network=100 --cpu=2 - sl cci list --network='< 100' --cpu=2 - sl cci list --memory='>= 2048' + sl vs list --network=100 --cpu=2 + sl vs list --network='< 100' --cpu=2 + sl vs list --memory='>= 2048' Note: Comparison operators (>, <, >=, <=) can be used with integers, floats, and strings. """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py index 11a2b5c6a..f374d82f5 100755 --- a/SoftLayer/CLI/modules/firewall.py +++ b/SoftLayer/CLI/modules/firewall.py @@ -4,30 +4,224 @@ Firewall rule and security management The available commands are: - list List active vlans with firewalls + add Add a new firewall + cancel Cancel an existing firewall + detail Provide details about a particular firewall + edit Edit the rules of a particular firewall + list List active firewalls - both dedicated and shared + """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. -from SoftLayer.CLI import CLIRunnable, Table, listing -from SoftLayer.CLI.helpers import blank -from SoftLayer import FirewallManager +from __future__ import print_function +from SoftLayer import FirewallManager, SoftLayerError +from SoftLayer.CLI import CLIRunnable, Table, listing, resolve_id, confirm +from SoftLayer.CLI.helpers import blank, CLIAbort +from subprocess import call +import os +import tempfile + +DELIMITER = "=========================================\n" + + +def get_ids(input_id): + """ Helper package to retrieve the actual IDs + :param input_id: the ID provided by the user + :returns: A list of valid IDs + """ + key_value = input_id.split(':') + + if len(key_value) != 2: + raise CLIAbort('Invalid ID %s: ID should be of the form xxx:yyy' + % input_id) + return key_value + + +def print_package_info(package): + """ Helper package to print the firewall price. + + :param dict package: A dictionary representing the firewall package + """ + print("******************") + print("Product: %s" % package[0]['description']) + print("Price: %s$ monthly" % package[0]['prices'][0]['recurringFee']) + print("******************") + return + + +def has_firewall_component(server): + """ Helper to determine whether or not a server has a firewall. + + :param dict server: A dictionary representing a server + :returns: True if the Server has a firewall. + """ + if server['status'] != 'no_edit': + return True + + return False + + +def get_rules_table(rules): + """ Helper to format the rules into a table + + :param list rules: A list containing the rules of the firewall + :returns: a formatted table of the firewall rules + """ + table = Table(['#', 'action', 'protocol', 'src_ip', 'src_mask', 'dest', + 'dest_mask']) + table.sortby = '#' + for rule in rules: + table.add_row([ + rule['orderValue'], + rule['action'], + rule['protocol'], + rule['sourceIpAddress'], + rule['sourceIpSubnetMask'], + '%s:%s-%s' % (rule['destinationIpAddress'], + rule['destinationPortRangeStart'], + rule['destinationPortRangeEnd']), + rule['destinationIpSubnetMask']]) + return table + + +def get_formatted_rule(rule=None): + """ Helper to format the rule into a user friendly format + for editing purposes + + :param dict rule: A dict containing one rule of the firewall + :returns: a formatted string that get be pushed into the editor + """ + rule = rule or {} + return ('action: %s\n' + 'protocol: %s\n' + 'source_ip_address: %s\n' + 'source_ip_subnet_mask: %s\n' + 'destination_ip_address: %s\n' + 'destination_ip_subnet_mask: %s\n' + 'destination_port_range_start: %s\n' + 'destination_port_range_end: %s\n' + 'version: %s\n' + % (rule.get('action', 'permit'), + rule.get('protocol', 'tcp'), + rule.get('sourceIpAddress', 'any'), + rule.get('sourceIpSubnetMask', '255.255.255.255'), + rule.get('destinationIpAddress', 'any'), + rule.get('destinationIpSubnetMask', '255.255.255.255'), + rule.get('destinationPortRangeStart', 1), + rule.get('destinationPortRangeEnd', 1), + rule.get('version', 4))) + + +def open_editor(rules=None, content=None): + """ Helper to open an editor for editing the firewall rules + This method takes two parameters, if content is provided, + that means that submitting the rules failed and we are allowing + the user to re-edit what they provided. + If content is not provided, the rules retrieved from the firewall + will be displayed to the user. + + :param list rules: A list containing the rules of the firewall + :param string content: the content that the user provided in the editor + :returns: a formatted string that get be pushed into the editor + """ + + # Let's get the default EDITOR of the environment, + # use nano if none is specified + editor = os.environ.get('EDITOR', 'nano') + + with tempfile.NamedTemporaryFile(suffix=".tmp") as tfile: + + if content: + # if content is provided, just display it as is + tfile.write(content) + tfile.flush() + call([editor, tfile.name]) + tfile.seek(0) + data = tfile.read() + return data + + if not rules: + # if the firewall has no rules, provide a template + tfile.write(DELIMITER) + tfile.write(get_formatted_rule()) + else: + # if the firewall has rules, display those to the user + for rule in rules: + tfile.write(DELIMITER) + tfile.write(get_formatted_rule(rule)) + tfile.write(DELIMITER) + tfile.flush() + call([editor, tfile.name]) + tfile.seek(0) + data = tfile.read() + return data + + return + + +def parse_rules(content=None): + """ Helper to parse the input from the user into a list of rules. + + :param string content: the content of the editor + :returns: a list of rules + """ + rules = content.split(DELIMITER) + parsed_rules = list() + order = 1 + for rule in rules: + if rule.strip() == '': + continue + parsed_rule = {} + lines = rule.split("\n") + parsed_rule['orderValue'] = order + order += 1 + for line in lines: + if line.strip() == '': + continue + key_value = line.strip().split(':') + key = key_value[0].strip() + value = key_value[1].strip() + if key == 'action': + parsed_rule['action'] = value + elif key == 'protocol': + parsed_rule['protocol'] = value + elif key == 'source_ip_address': + parsed_rule['sourceIpAddress'] = value + elif key == 'source_ip_subnet_mask': + parsed_rule['sourceIpSubnetMask'] = value + elif key == 'destination_ip_address': + parsed_rule['destinationIpAddress'] = value + elif key == 'destination_ip_subnet_mask': + parsed_rule['destinationIpSubnetMask'] = value + elif key == 'destination_port_range_start': + parsed_rule['destinationPortRangeStart'] = int(value) + elif key == 'destination_port_range_end': + parsed_rule['destinationPortRangeEnd'] = int(value) + elif key == 'version': + parsed_rule['version'] = int(value) + parsed_rules.append(parsed_rule) + return parsed_rules class FWList(CLIRunnable): """ usage: sl firewall list [options] -List active vlans with firewalls +List active firewalls """ action = 'list' def execute(self, args): - f = FirewallManager(self.client) - fwvlans = f.get_firewalls() - t = Table(['vlan', 'type', 'features']) + mgr = FirewallManager(self.client) + table = Table(['firewall id', + 'type', + 'features', + 'server/vlan id']) + + fwvlans = mgr.get_firewalls() + dedicatedfws = [firewall for firewall in fwvlans + if firewall['dedicatedFirewallFlag']] - dedicatedfws = filter(lambda x: x['dedicatedFirewallFlag'], fwvlans) for vlan in dedicatedfws: features = [] if vlan['highAvailabilityFirewallFlag']: @@ -38,14 +232,184 @@ def execute(self, args): else: feature_list = blank() - t.add_row([ - vlan['vlanNumber'], - 'dedicated', + table.add_row([ + 'vlan:%s' % vlan['networkVlanFirewall']['id'], + 'VLAN - dedicated', feature_list, + vlan['id'] ]) - shared_vlan = filter(lambda x: not x['dedicatedFirewallFlag'], fwvlans) + shared_vlan = [firewall for firewall in fwvlans + if not firewall['dedicatedFirewallFlag']] for vlan in shared_vlan: - t.add_row([vlan['vlanNumber'], 'standard', blank()]) + fwls = [guest for guest in vlan['firewallGuestNetworkComponents'] + if has_firewall_component(guest)] + + for firewall in fwls: + table.add_row([ + 'cci:%s' % firewall['id'], + 'CCI - standard', + '-', + firewall['guestNetworkComponent']['guest']['id'] + ]) + + fwls = [server for server in vlan['firewallNetworkComponents'] + if has_firewall_component(server)] + + for fwl in fwls: + table.add_row([ + 'server:%s' % fwl['id'], + 'Server - standard', + '-', + fwl['networkComponent']['downlinkComponent']['hardwareId'] + ]) + + return table + + +class FWCancel(CLIRunnable): + """ +usage: sl firewall cancel [options] + +Cancels a firewall + +Options: + --really Whether to skip the confirmation prompt + +""" + action = 'cancel' + options = ['really'] + + def execute(self, args): + mgr = FirewallManager(self.client) + input_id = args.get('') + key_value = get_ids(input_id) + firewall_id = int(key_value[1]) + + if args['--really'] or confirm("This action will cancel a firewall" + " from your account. Continue?"): + if key_value[0] in ['cci', 'server']: + mgr.cancel_firewall(firewall_id, dedicated=False) + elif key_value[0] == 'vlan': + mgr.cancel_firewall(firewall_id, dedicated=True) + return 'Firewall with id %s is being cancelled!' % input_id + else: + raise CLIAbort('Aborted.') + + +class FWAdd(CLIRunnable): + """ +usage: sl firewall add (--cci | --vlan | --server) [options] + +Adds a firewall of type either standard (cci or server) or dedicated(vlan) +Options: + --cci creates a standard firewall for a CCI + --vlan creates a dedicated firewall for a VLAN + --server creates a standard firewall for a server + --ha whether HA will be on or off - only for dedicated + --really whether to skip the confirmation prompt +""" + action = 'add' + options = ['really', 'ha'] + + def execute(self, args): + mgr = FirewallManager(self.client) + input_id = resolve_id( + mgr.resolve_ids, args.get(''), 'firewall') + ha_support = args.get('--ha', False) + if not args['--really']: + if args['--vlan']: + pkg = mgr.get_dedicated_package(ha_enabled=ha_support) + elif args['--cci']: + pkg = mgr.get_standard_package(input_id) + elif args['--server']: + pkg = mgr.get_standard_package(input_id, is_cci=False) + + if not pkg: + return "Unable to add firewall - Is network public enabled?" + print_package_info(pkg) + + if not confirm("This action will incur charges on your account. " + "Continue?"): + raise CLIAbort('Aborted.') + + if args['--vlan']: + mgr.add_vlan_firewall(input_id, ha_enabled=ha_support) + elif args['--cci']: + mgr.add_standard_firewall(input_id, is_cci=True) + elif args['--server']: + mgr.add_standard_firewall(input_id, is_cci=False) + + return "Firewall is being created!" + + +class FWDetails(CLIRunnable): + """ +usage: sl firewall detail [options] + +Get firewall details +""" + action = 'detail' + + def execute(self, args): + mgr = FirewallManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + if key_value[0] == 'vlan': + rules = mgr.get_dedicated_fwl_rules(key_value[1]) + else: + rules = mgr.get_standard_fwl_rules(key_value[1]) + + return get_rules_table(rules) + + +class FWEdit(CLIRunnable): + """ +usage: sl firewall edit [options] + +Edit the rules for a firewall +""" + action = 'edit' + + def execute(self, args): + mgr = FirewallManager(self.client) + input_id = args.get('') - return t + key_value = get_ids(input_id) + firewall_id = int(key_value[1]) + if key_value[0] == 'vlan': + orig_rules = mgr.get_dedicated_fwl_rules(firewall_id) + else: + orig_rules = mgr.get_standard_fwl_rules(firewall_id) + # open an editor for the user to enter their rules + edited_rules = open_editor(rules=orig_rules) + print(edited_rules) + if confirm("Would you like to submit the rules. " + "Continue?"): + while True: + try: + rules = parse_rules(edited_rules) + if key_value[0] == 'vlan': + rules = mgr.edit_dedicated_fwl_rules(firewall_id, + rules) + else: + rules = mgr.edit_standard_fwl_rules(firewall_id, + rules) + break + except (SoftLayerError, ValueError) as error: + print("Unexpected error({%s})" % (error)) + if confirm("Would you like to continue editing the rules" + ". Continue?"): + edited_rules = open_editor(content=edited_rules) + print(edited_rules) + if confirm("Would you like to submit the rules. " + "Continue?"): + continue + else: + raise CLIAbort('Aborted.') + else: + raise CLIAbort('Aborted.') + return 'Firewall updated!' + else: + raise CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/modules/globalip.py b/SoftLayer/CLI/modules/globalip.py index 7873f90c6..144eb0a0f 100644 --- a/SoftLayer/CLI/modules/globalip.py +++ b/SoftLayer/CLI/modules/globalip.py @@ -10,13 +10,12 @@ list Display a list of global IP addresses unassign Unassigns a global IP """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. from SoftLayer import NetworkManager from SoftLayer.CLI import ( - CLIRunnable, Table, FormattedItem, confirm, no_going_back) -from SoftLayer.CLI.helpers import CLIAbort, SequentialOutput + CLIRunnable, Table, confirm, no_going_back, resolve_id) +from SoftLayer.CLI.helpers import CLIAbort class GlobalIpAssign(CLIRunnable): @@ -33,8 +32,9 @@ class GlobalIpAssign(CLIRunnable): def execute(self, args): mgr = NetworkManager(self.client) - - global_ip_id = mgr.resolve_global_ip_ids(args.get('')) + global_ip_id = resolve_id(mgr.resolve_global_ip_ids, + args.get(''), + name='global ip') if not global_ip_id: raise CLIAbort("Unable to find global IP record for " + args['']) @@ -53,7 +53,9 @@ class GlobalIpCancel(CLIRunnable): def execute(self, args): mgr = NetworkManager(self.client) - global_ip_id = mgr.resolve_global_ip_ids(args.get('')) + global_ip_id = resolve_id(mgr.resolve_global_ip_ids, + args.get(''), + name='global ip') if args['--really'] or no_going_back(global_ip_id): mgr.cancel_global_ip(global_ip_id) @@ -89,26 +91,19 @@ def execute(self, args): test_order=args.get('--test')) if not result: return 'Unable to place order: No valid price IDs found.' - t = Table(['Item', 'cost']) - t.align['Item'] = 'r' - t.align['cost'] = 'r' + table = Table(['Item', 'cost']) + table.align['Item'] = 'r' + table.align['cost'] = 'r' total = 0.0 for price in result['orderDetails']['prices']: total += float(price.get('recurringFee', 0.0)) rate = "%.2f" % float(price['recurringFee']) - t.add_row([price['item']['description'], rate]) + table.add_row([price['item']['description'], rate]) - t.add_row(['Total monthly cost', "%.2f" % total]) - output = SequentialOutput() - output.append(t) - output.append(FormattedItem( - '', - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guarenteed.') - ) - return t + table.add_row(['Total monthly cost', "%.2f" % total]) + return table class GlobalIpList(CLIRunnable): @@ -126,10 +121,10 @@ class GlobalIpList(CLIRunnable): def execute(self, args): mgr = NetworkManager(self.client) - t = Table([ + table = Table([ 'id', 'ip', 'assigned', 'target' ]) - t.sortby = args.get('--sortby') or 'id' + table.sortby = args.get('--sortby') or 'id' version = 0 if args.get('--v4'): @@ -139,24 +134,27 @@ def execute(self, args): ips = mgr.list_global_ips(version=version) - for ip in ips: + for ip_address in ips: assigned = 'No' target = 'None' - if ip.get('destinationIpAddress'): - dest = ip['destinationIpAddress'] + if ip_address.get('destinationIpAddress'): + dest = ip_address['destinationIpAddress'] assigned = 'Yes' target = dest['ipAddress'] - if dest.get('virtualGuest'): - vg = dest['virtualGuest'] - target += ' (' + vg['fullyQualifiedDomainName'] + ')' - elif ip['destinationIpAddress'].get('hardware'): + virtual_guest = dest.get('virtualGuest') + if virtual_guest: + target += (' (%s)' + % virtual_guest['fullyQualifiedDomainName']) + elif ip_address['destinationIpAddress'].get('hardware'): target += ' (' + \ dest['hardware']['fullyQualifiedDomainName'] + \ ')' - t.add_row([ip['id'], ip['ipAddress']['ipAddress'], assigned, - target]) - return t + table.add_row([ip_address['id'], + ip_address['ipAddress']['ipAddress'], + assigned, + target]) + return table class GlobalIpUnassign(CLIRunnable): @@ -172,8 +170,9 @@ class GlobalIpUnassign(CLIRunnable): def execute(self, args): mgr = NetworkManager(self.client) - - global_ip_id = mgr.resolve_global_ip_ids(args.get('')) + global_ip_id = resolve_id(mgr.resolve_global_ip_ids, + args.get(''), + name='global ip') if not global_ip_id: raise CLIAbort("Unable to find global IP record for " + args['']) diff --git a/SoftLayer/CLI/modules/help.py b/SoftLayer/CLI/modules/help.py index f490a76de..2bf5fae83 100644 --- a/SoftLayer/CLI/modules/help.py +++ b/SoftLayer/CLI/modules/help.py @@ -5,8 +5,9 @@ View help on a module or command. """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. +# Missing docstrings ignored due to __doc__ = __doc__ magic +# pylint: disable=C0111 from SoftLayer.CLI.core import CommandParser from SoftLayer.CLI import CLIRunnable @@ -19,9 +20,12 @@ class Show(CLIRunnable): def execute(self, args): parser = CommandParser(self.env) + if not any([args[''], args['']]): + return parser.get_module_help('help') + self.env.load_module(args['']) + if args['']: return parser.get_command_help(args[''], args['']) elif args['']: return parser.get_module_help(args['']) - return parser.get_main_help() diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py index 49b610ee0..099893ba2 100644 --- a/SoftLayer/CLI/modules/image.py +++ b/SoftLayer/CLI/modules/image.py @@ -1,16 +1,19 @@ """ usage: sl image [] [...] [options] -Manage compute and flex images +Manage compute images The available commands are: - list List images + delete Delete an image + detail Output details about an image + list List images + edit Edit an image """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. -from SoftLayer.CLI import CLIRunnable, Table -from SoftLayer.CLI.helpers import blank +from SoftLayer import ImageManager +from SoftLayer.CLI import CLIRunnable, Table, KeyValueTable, blank, resolve_id +from SoftLayer.CLI.helpers import CLIAbort class ListImages(CLIRunnable): @@ -26,36 +29,112 @@ class ListImages(CLIRunnable): action = 'list' def execute(self, args): - account = self.client['Account'] + image_mgr = ImageManager(self.client) neither = not any([args['--private'], args['--public']]) + mask = 'id,accountId,name,globalIdentifier,blockDevices,parentId' - results = [] + images = [] if args['--private'] or neither: - account = self.client['Account'] - mask = 'id,accountId,name,globalIdentifier,blockDevices,parentId' - r = account.getPrivateBlockDeviceTemplateGroups(mask=mask) - - results.append(r) + for image in image_mgr.list_private_images(mask=mask): + image['visibility'] = 'private' + images.append(image) if args['--public'] or neither: - vgbd = self.client['Virtual_Guest_Block_Device_Template_Group'] - r = vgbd.getPublicImages() - - results.append(r) - - t = Table(['id', 'account', 'type', 'name', 'guid', ]) - t.sortby = 'name' - - for result in results: - images = filter(lambda x: x['parentId'] == '', result) - for image in images: - t.add_row([ - image['id'], - image.get('accountId', blank()), - image.get('type', blank()), - image['name'].strip(), - image.get('globalIdentifier', blank()), - ]) - - return t + for image in image_mgr.list_public_images(mask=mask): + image['visibility'] = 'public' + images.append(image) + + table = Table(['id', + 'account', + 'visibility', + 'name', + 'global_identifier']) + + images = [image for image in images if image['parentId'] == ''] + for image in images: + table.add_row([ + image['id'], + image.get('accountId', blank()), + image['visibility'], + image['name'].strip(), + image.get('globalIdentifier', blank()), + ]) + + return table + + +class DetailImage(CLIRunnable): + """ +usage: sl image detail [options] + +Get details for an image +""" + action = 'detail' + + def execute(self, args): + image_mgr = ImageManager(self.client) + image_id = resolve_id(image_mgr.resolve_ids, + args.get(''), + 'image') + + image = image_mgr.get_image(image_id) + + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + table.add_row(['id', image['id']]) + table.add_row(['account', image.get('accountId', blank())]) + table.add_row(['name', image['name'].strip()]) + table.add_row(['global_identifier', + image.get('globalIdentifier', blank())]) + + return table + + +class DeleteImage(CLIRunnable): + """ +usage: sl image delete [options] + +Get details for an image +""" + action = 'delete' + + def execute(self, args): + image_mgr = ImageManager(self.client) + image_id = resolve_id(image_mgr.resolve_ids, + args.get(''), + 'image') + + image_mgr.delete_image(image_id) + + +class EditImage(CLIRunnable): + """ +usage: sl image edit [--tag=Tag...] [options] + +Edit Details for an image + +Options: + --name=Name Name of the Image + --note=Note Note of the Image + --tag=TAG... Tags of the Image. Can be specified multiple times. + +Note: Image to be edited must be private +""" + action = 'edit' + + def execute(self, args): + image_mgr = ImageManager(self.client) + data = {} + if args.get('--name'): + data['name'] = args.get('--name') + if args.get('--note'): + data['note'] = args.get('--note') + if args.get('--tag'): + data['tag'] = args.get('--tag') + image_id = resolve_id(image_mgr.resolve_ids, + args.get(''), 'image') + if not image_mgr.edit(image_id, **data): + raise CLIAbort("Failed to Edit Image") diff --git a/SoftLayer/CLI/modules/iscsi.py b/SoftLayer/CLI/modules/iscsi.py index 04c1bf238..fe83b4d1f 100644 --- a/SoftLayer/CLI/modules/iscsi.py +++ b/SoftLayer/CLI/modules/iscsi.py @@ -1,34 +1,40 @@ """ usage: sl iscsi [] [...] [options] -Manage iSCSI targets +Manage, order, delete iSCSI targets The available commands are: - list List iSCSI targets + cancel Cancel an existing iSCSI target + create Order and create an iSCSI target + detail Output details about an iSCSI + list List iSCSI targets on the account + +For several commands, will be asked for. This will be the id +for iSCSI target. """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. -# :license: MIT, see LICENSE for more details. +from SoftLayer.CLI import (CLIRunnable, Table, no_going_back, FormattedItem) +from SoftLayer.CLI.helpers import ( + CLIAbort, ArgumentError, NestedDict, blank, resolve_id, KeyValueTable) +from SoftLayer import ISCSIManager -from SoftLayer.CLI import CLIRunnable, Table, FormattedItem -from SoftLayer.CLI.helpers import NestedDict, blank +class ListISCSIs(CLIRunnable): -class ListISCSI(CLIRunnable): """ usage: sl iscsi list [options] -List iSCSI accounts +List iSCSI targets """ action = 'list' def execute(self, args): account = self.client['Account'] - iscsi = account.getIscsiNetworkStorage( + iscsi_list = account.getIscsiNetworkStorage( mask='eventCount,serviceResource[datacenter.name]') - iscsi = [NestedDict(n) for n in iscsi] + iscsi_list = [NestedDict(n) for n in iscsi_list] - t = Table([ + table = Table([ 'id', 'datacenter', 'size', @@ -37,15 +43,147 @@ def execute(self, args): 'server' ]) - for n in iscsi: - t.add_row([ - n['id'], - n['serviceResource']['datacenter'].get('name', blank()), + for iscsi in iscsi_list: + table.add_row([ + iscsi['id'], + iscsi['serviceResource']['datacenter'].get('name', blank()), FormattedItem( - n.get('capacityGb', blank()), - "%dGB" % n.get('capacityGb', 0)), - n.get('username', blank()), - n.get('password', blank()), - n.get('serviceResourceBackendIpAddress', blank())]) + iscsi.get('capacityGb', blank()), + "%dGB" % iscsi.get('capacityGb', 0)), + iscsi.get('username', blank()), + iscsi.get('password', blank()), + iscsi.get('serviceResourceBackendIpAddress', blank())]) + + return table + + +class CreateISCSI(CLIRunnable): + + """ +usage: sl iscsi create [options] + +Orders and creates an iSCSI target. + +Examples: + sl iscsi create --size=1 --datacenter=dal05 + sl iscsi create --size 1 -d dal05 + sl iscsi create -s 1 -d dal05 + +Required: + -s, --size=SIZE Size of the iSCSI volume to create + -d, --datacenter=DC Datacenter shortname (sng01, dal05, ...) +""" + action = 'create' + options = ['confirm'] + required_params = ['--size', '--datacenter'] + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + self._validate_create_args(args) + size, location = self._parse_create_args(args) + iscsi_mgr.create_iscsi(size=size, location=location) + + def _parse_create_args(self, args): + """ Converts CLI arguments to arguments that can be passed into + ISCSIManager.create_iscsi. + :param dict args: CLI arguments + """ + size = args['--size'] + location = args['--datacenter'] + return int(size), str(location) + + def _validate_create_args(self, args): + """ Raises an ArgumentError if the given arguments are not valid """ + invalid_args = [k for k in self.required_params if args.get(k) is None] + if invalid_args: + raise ArgumentError('Missing required options: %s' + % ','.join(invalid_args)) + + +class CancelISCSI(CLIRunnable): + + """ +usage: sl iscsi cancel [options] + +Cancel existing iSCSI + +Examples: + sl iscsi cancel 12345 + sl iscsi cancel 12345 --immediate + sl iscsi cancel 12345 --immediate --reason='no longer needed' + +options : + --immediate Cancels the iSCSI immediately (instead of on the billing + anniversary) + --reason=REASON An optional reason for cancellation. +""" + action = 'cancel' + options = ['confirm'] + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + iscsi_id = resolve_id( + iscsi_mgr.resolve_ids, + args.get(''), + 'iSCSI') + + immediate = args.get('--immediate', False) + + reason = args.get('--reason') + if args['--really'] or no_going_back(iscsi_id): + iscsi_mgr.cancel_iscsi(iscsi_id, reason, immediate) + else: + CLIAbort('Aborted') + + +class ISCSIDetails(CLIRunnable): + + """ +usage: sl iscsi detail [--password] [options] + +Get details for an iSCSI + +Examples: + sl iscsi detail 12345 + sl iscsi detail 12345 --password + +Options: + --password Show credentials to access the iSCSI target +""" + action = 'detail' + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + iscsi_id = resolve_id( + iscsi_mgr.resolve_ids, + args.get(''), + 'iSCSI') + result = iscsi_mgr.get_iscsi(iscsi_id) + result = NestedDict(result) + + table.add_row(['id', result['id']]) + table.add_row(['serviceResourceName', result['serviceResourceName']]) + table.add_row(['createDate', result['createDate']]) + table.add_row(['nasType', result['nasType']]) + table.add_row(['capacityGb', result['capacityGb']]) + if result['snapshotCapacityGb']: + table.add_row(['snapshotCapacityGb', result['snapshotCapacityGb']]) + table.add_row(['mountableFlag', result['mountableFlag']]) + table.add_row( + ['serviceResourceBackendIpAddress', + result['serviceResourceBackendIpAddress']]) + table.add_row(['price', result['billingItem']['recurringFee']]) + table.add_row(['BillingItemId', result['billingItem']['id']]) + if result.get('notes'): + table.add_row(['notes', result['notes']]) + + if args.get('--password'): + pass_table = Table(['username', 'password']) + pass_table.add_row([result['username'], result['password']]) + table.add_row(['users', pass_table]) - return t + return table diff --git a/SoftLayer/CLI/modules/loadbal.py b/SoftLayer/CLI/modules/loadbal.py new file mode 100755 index 000000000..2222832d4 --- /dev/null +++ b/SoftLayer/CLI/modules/loadbal.py @@ -0,0 +1,580 @@ +""" +usage: sl loadbal [] [...] [options] + +Local LoadBalancer management + +The available commands are: + cancel Cancel an existing load balancer + create Create a new load balancer + create-options Lists the different packages for load balancers + detail Provide details about a particular load balancer + group-add Add a new service group in the load balancer + group-delete Delete a service group from the load balancer + group-edit Edit the properties of a service group + group-reset Resets all the connections on a service group + health-checks List the different health check values + list List active load balancers + routing-methods List supported routing methods + routing-types List supported routing types + service-add Add a service to an existing service group + service-delete Delete an existing service + service-edit Edit an existing service + service-toggle Toggle the status of the service +""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer import LoadBalancerManager +from SoftLayer.CLI import (CLIRunnable, Table, resolve_id, + confirm, KeyValueTable) +from SoftLayer.CLI.helpers import CLIAbort + + +def get_ids(input_id): + """ Helper package to retrieve the actual IDs + + :param input_id: the ID provided by the user + :returns: A list of valid IDs + """ + key_value = input_id.split(':') + if len(key_value) != 2: + raise CLIAbort('Invalid ID %s: ID should be of the form xxx:yyy' + % input_id) + return key_value + + +def get_local_lbs_table(load_balancers): + """ Helper package to format the local load balancers into a table. + + :param dict load_balancers: A dictionary representing the load_balancers + :returns: A table containing the local load balancers + """ + table = Table(['ID', + 'VIP Address', + 'Location', + 'SSL Offload', + 'Connections/second', + 'Type']) + + table.align['Connections/second'] = 'r' + + for load_balancer in load_balancers: + ssl_support = 'Not Supported' + if load_balancer['sslEnabledFlag']: + if load_balancer['sslActiveFlag']: + ssl_support = 'On' + else: + ssl_support = 'Off' + lb_type = 'Standard' + if load_balancer['dedicatedFlag']: + lb_type = 'Dedicated' + elif load_balancer['highAvailabilityFlag']: + lb_type = 'HA' + table.add_row([ + 'local:%s' % load_balancer['id'], + load_balancer['ipAddress']['ipAddress'], + load_balancer['loadBalancerHardware'][0]['datacenter']['name'], + ssl_support, + load_balancer['connectionLimit'], + lb_type + ]) + return table + + +def get_local_lb_table(load_balancer): + """ Helper package to format the local loadbal details into a table. + + :param dict load_balancer: A dictionary representing the loadbal + :returns: A table containing the local loadbal details + """ + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'l' + table.align['Value'] = 'l' + table.add_row(['General properties', '----------']) + table.add_row([' ID', 'local:%s' % load_balancer['id']]) + table.add_row([' IP Address', load_balancer['ipAddress']['ipAddress']]) + name = load_balancer['loadBalancerHardware'][0]['datacenter']['name'] + table.add_row([' Datacenter', name]) + table.add_row([' Connections limit', load_balancer['connectionLimit']]) + table.add_row([' Dedicated', load_balancer['dedicatedFlag']]) + table.add_row([' HA', load_balancer['highAvailabilityFlag']]) + table.add_row([' SSL Enabled', load_balancer['sslEnabledFlag']]) + table.add_row([' SSL Active', load_balancer['sslActiveFlag']]) + index0 = 1 + for virtual_server in load_balancer['virtualServers']: + table.add_row(['Service group %s' % index0, + '**************']) + index0 += 1 + table2 = Table(['Service group ID', 'Port', 'Allocation', + 'Routing type', 'Routing Method']) + + for group in virtual_server['serviceGroups']: + table2.add_row([ + '%s:%s' % (load_balancer['id'], virtual_server['id']), + virtual_server['port'], + '%s %%' % virtual_server['allocation'], + '%s:%s' % (group['routingTypeId'], + group['routingType']['name']), + '%s:%s' % (group['routingMethodId'], + group['routingMethod']['name']) + ]) + + table.add_row([' Group Properties', table2]) + + table3 = Table(['Service_ID', 'IP Address', 'Port', + 'Health Check', 'Weight', 'Enabled', 'Status']) + service_exist = False + for service in group['services']: + service_exist = True + health_check = service['healthChecks'][0] + table3.add_row([ + '%s:%s' % (load_balancer['id'], service['id']), + service['ipAddress']['ipAddress'], + service['port'], + '%s:%s' % (health_check['healthCheckTypeId'], + health_check['type']['name']), + service['groupReferences'][0]['weight'], + service['enabled'], + service['status'] + ]) + if service_exist: + table.add_row([' Services', table3]) + else: + table.add_row([' Services', 'None']) + return table + + +class LoadBalancerList(CLIRunnable): + """ +usage: sl loadbal list [options] + +List active load balancers + +""" + action = 'list' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + + load_balancers = mgr.get_local_lbs() + return get_local_lbs_table(load_balancers) + + +class LoadBalancerHealthChecks(CLIRunnable): + """ +usage: sl loadbal health-checks [options] + +List load balancer service health check types that can be used +""" + action = 'health-checks' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + + hc_types = mgr.get_hc_types() + table = KeyValueTable(['ID', 'Name']) + table.align['ID'] = 'l' + table.align['Name'] = 'l' + table.sortby = 'ID' + for hc_type in hc_types: + table.add_row([hc_type['id'], hc_type['name']]) + return table + + +class LoadBalancerRoutingMethods(CLIRunnable): + """ +usage: sl loadbal routing-methods [options] + +List load balancers routing methods that can be used +""" + action = 'routing-methods' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + + routing_methods = mgr.get_routing_methods() + table = KeyValueTable(['ID', 'Name']) + table.align['ID'] = 'l' + table.align['Name'] = 'l' + table.sortby = 'ID' + for routing_method in routing_methods: + table.add_row([routing_method['id'], routing_method['name']]) + return table + + +class LoadBalancerRoutingTypes(CLIRunnable): + """ +usage: sl loadbal routing-types [options] + +List load balancers routing types that can be used +""" + action = 'routing-types' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + + routing_types = mgr.get_routing_types() + table = KeyValueTable(['ID', 'Name']) + table.align['ID'] = 'l' + table.align['Name'] = 'l' + table.sortby = 'ID' + for routing_type in routing_types: + table.add_row([routing_type['id'], routing_type['name']]) + return table + + +class LoadBalancerDetails(CLIRunnable): + """ +usage: sl loadbal detail [options] + +Get Load balancer details + +""" + action = 'detail' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + + input_id = args.get('') + + key_value = get_ids(input_id) + loadbal_id = int(key_value[1]) + + load_balancer = mgr.get_local_lb(loadbal_id) + return get_local_lb_table(load_balancer) + + +class LoadBalancerCancel(CLIRunnable): + """ +usage: sl loadbal cancel [options] + +Cancels an existing load_balancer + +""" + action = 'cancel' + options = ['confirm'] + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + loadbal_id = int(key_value[1]) + + if args['--really'] or confirm("This action will cancel a load " + "balancer. Continue?"): + mgr.cancel_lb(loadbal_id) + return 'Load Balancer with id %s is being cancelled!' % input_id + else: + raise CLIAbort('Aborted.') + + +class LoadBalancerServiceDelete(CLIRunnable): + """ +usage: sl loadbal service-delete [options] + +Deletes an existing load_balancer service + +""" + action = 'service-delete' + options = ['confirm'] + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + service_id = int(key_value[1]) + + if args['--really'] or confirm("This action will cancel a service " + "from your load balancer. Continue?"): + mgr.delete_service(service_id) + return 'Load balancer service %s is being cancelled!' % input_id + else: + raise CLIAbort('Aborted.') + + +class LoadBalancerServiceToggle(CLIRunnable): + """ +usage: sl loadbal service-toggle [options] + +Toggle the status of an existing load_balancer service + +""" + action = 'service-toggle' + options = ['confirm'] + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + service_id = int(key_value[1]) + + if args['--really'] or confirm("This action will toggle the service " + "status on the service. Continue?"): + mgr.toggle_service_status(service_id) + return 'Load balancer service %s status updated!' % input_id + else: + raise CLIAbort('Aborted.') + + +class LoadBalancerServiceEdit(CLIRunnable): + """ +usage: sl loadbal service-edit [options] + +Enable an existing load_balancer service +Options: +--enabled=ENABLED Set to 1 to enable the service, or 0 to disable +--port=PORT Change the value of the port +--weight=WEIGHT Change the weight of the service +--hc_type=HCTYPE Change the health check type +--ip=IP Change the IP of the service + +""" + action = 'service-edit' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + loadbal_id = int(key_value[0]) + service_id = int(key_value[1]) + + # check if any input is provided + if not (args['--ip'] or args['--enabled'] or args['--weight'] + or args['--port'] or args['--hc_type']): + return 'At least one property is required to be changed!' + + # check if the IP is valid + ip_address_id = None + if args['--ip']: + ip_address = mgr.get_ip_address(args['--ip']) + if not ip_address: + return 'Provided IP address is not valid!' + else: + ip_address_id = ip_address['id'] + + mgr.edit_service(loadbal_id, + service_id, + ip_address_id=ip_address_id, + enabled=args.get('--enabled'), + port=args.get('--port'), + weight=args.get('--weight'), + hc_type=args.get('--hc_type')) + return 'Load balancer service %s is being modified!' % input_id + + +class LoadBalancerServiceAdd(CLIRunnable): + """ +usage: sl loadbal service-add --ip=IP --port=PORT \ +--weight=WEIGHT --hc_type=HCTYPE --enabled=ENABLED [options] + +Adds a new load_balancer service +Required: +--enabled=ENABLED Set to 1 to enable the service, 0 to disable [default: 1]. +--port=PORT Set to the desired port value [default: 80]. +--weight=WEIGHT Set to the desired weight value [default: 1]. +--hc_type=HCTYPE Set to the desired health check value [default: 21]. +--ip=IP Set to the desired IP value. + +""" + action = 'service-add' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + loadbal_id = int(key_value[0]) + group_id = int(key_value[1]) + + # check if the IP is valid + ip_address = None + if args['--ip']: + ip_address = mgr.get_ip_address(args['--ip']) + if not ip_address: + return 'Provided IP address is not valid!' + + mgr.add_service(loadbal_id, + group_id, + ip_address_id=ip_address['id'], + enabled=args.get('--enabled'), + port=args.get('--port'), + weight=args.get('--weight'), + hc_type=args.get('--hc_type')) + return 'Load balancer service is being added!' + + +class LoadBalancerServiceGroupDelete(CLIRunnable): + """ +usage: sl loadbal group-delete [options] + +Deletes an existing load_balancer service group + +""" + action = 'group-delete' + options = ['confirm'] + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + group_id = int(key_value[1]) + + if args['--really'] or confirm("This action will cancel a service" + " group. Continue?"): + mgr.delete_service_group(group_id) + return 'Service group %s is being deleted!' % input_id + else: + raise CLIAbort('Aborted.') + + +class LoadBalancerServiceGroupEdit(CLIRunnable): + """ +usage: sl loadbal group-edit [options] + +Edits an existing load_balancer service group +Options: +--allocation=PERC Change the allocated % of connections +--port=PORT Change the port +--routing_type=TYPE Change the port routing type +--routing_method=METHOD Change the routing method + +""" + action = 'group-edit' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + loadbal_id = int(key_value[0]) + group_id = int(key_value[1]) + + # check if any input is provided + if not (args['--allocation'] or args['--port'] + or args['--routing_type'] or args['--routing_method']): + return 'At least one property is required to be changed!' + + routing_type = args.get('--routing_type') + routing_method = args.get('--routing_method') + + mgr.edit_service_group(loadbal_id, + group_id, + allocation=args.get('--allocation'), + port=args.get('--port'), + routing_type=routing_type, + routing_method=routing_method) + + return 'Load balancer service group %s is being updated!' % input_id + + +class LoadBalancerServiceGroupReset(CLIRunnable): + """ +usage: sl loadbal group-reset [options] + +Resets the connections on a certain service group + +""" + action = 'group-reset' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + + key_value = get_ids(input_id) + loadbal_id = int(key_value[0]) + group_id = int(key_value[1]) + + mgr.reset_service_group(loadbal_id, group_id) + return 'Load balancer service group connections are being reset!' + + +class LoadBalancerServiceGroupAdd(CLIRunnable): + """ +usage: sl loadbal group-add --allocation=PERC --port=PORT \ +--routing_type=TYPE --routing_method=METHOD [options] + +Adds a new load_balancer service +Required: +--allocation=PERC The % of connections that will be allocated +--port=PORT The virtual port number for the group +--routing_type=TYPE The routing type for the group +--routing_method=METHOD The routing method for the group + +""" + action = 'group-add' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = args.get('') + key_value = get_ids(input_id) + + loadbal_id = int(key_value[1]) + + mgr.add_service_group(loadbal_id, + allocation=int(args.get('--allocation')), + port=int(args.get('--port')), + routing_type=int(args.get('--routing_type')), + routing_method=int(args.get('--routing_method'))) + + return 'Load balancer service group is being added!' + + +class LoadBalancerCreate(CLIRunnable): + """ +usage: sl loadbal create (--datacenter=DC) [options] + +Adds a load_balancer given the billing id returned from create-options + +Options: + -d, --datacenter=DC Datacenter shortname (sng01, dal05, ...) + Note: Omitting this value defaults to the first + available datacenter +""" + action = 'create' + options = ['confirm'] + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + input_id = resolve_id( + mgr.resolve_ids, args.get(''), 'load_balancer') + if not confirm("This action will incur charges on your account. " + "Continue?"): + raise CLIAbort('Aborted.') + mgr.add_local_lb(input_id, datacenter=args['--datacenter']) + return "Load balancer is being created!" + + +class CreateOptionsLoadBalancer(CLIRunnable): + """ +usage: sl loadbal create-options + +Output available options when adding a new load balancer + +""" + action = 'create-options' + + def execute(self, args): + mgr = LoadBalancerManager(self.client) + + table = Table(['id', 'capacity', 'description', 'price']) + + table.sortby = 'price' + table.align['price'] = 'r' + table.align['capacity'] = 'r' + table.align['id'] = 'r' + + packages = mgr.get_lb_pkgs() + + for package in packages: + table.add_row([ + package['prices'][0]['id'], + package.get('capacity'), + package['description'], + format(float(package['prices'][0]['recurringFee']), '.2f') + ]) + + return table diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py index ae2964c21..625466f8d 100644 --- a/SoftLayer/CLI/modules/messaging.py +++ b/SoftLayer/CLI/modules/messaging.py @@ -26,8 +26,9 @@ topic-unsubscribe Remove a subscription on a topic """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. +# Missing docstrings ignored due to __doc__ = __doc__ magic +# pylint: disable=C0111 import sys from SoftLayer import MessagingManager @@ -54,20 +55,20 @@ def execute(self, args): manager = MessagingManager(self.client) accounts = manager.list_accounts() - t = Table([ + table = Table([ 'id', 'name', 'status' ]) for account in accounts: if not account['nodes']: continue - t.add_row([ + table.add_row([ account['nodes'][0]['accountName'], account['name'], account['status']['name'], ]) - return t + return table class ListEndpoints(CLIRunnable): @@ -83,17 +84,17 @@ def execute(self, args): manager = MessagingManager(self.client) regions = manager.get_endpoints() - t = Table([ + table = Table([ 'name', 'public', 'private' ]) for region, endpoints in regions.items(): - t.add_row([ + table.add_row([ region, endpoints.get('public') or blank(), endpoints.get('private') or blank(), ]) - return t + return table class Ping(CLIRunnable): @@ -107,61 +108,65 @@ class Ping(CLIRunnable): def execute(self, args): manager = MessagingManager(self.client) - ok = manager.ping( + okay = manager.ping( datacenter=args['--datacenter'], network=args['--network']) - if ok: + if okay: return 'OK' else: CLIAbort('Ping failed') def queue_table(queue): - t = Table(['property', 'value']) - t.align['property'] = 'r' - t.align['value'] = 'l' + """ Returns a table with details about a queue """ + table = Table(['property', 'value']) + table.align['property'] = 'r' + table.align['value'] = 'l' - t.add_row(['name', queue['name']]) - t.add_row(['message_count', queue['message_count']]) - t.add_row(['visible_message_count', queue['visible_message_count']]) - t.add_row(['tags', listing(queue['tags'] or [])]) - t.add_row(['expiration', queue['expiration']]) - t.add_row(['visibility_interval', queue['visibility_interval']]) - return t + table.add_row(['name', queue['name']]) + table.add_row(['message_count', queue['message_count']]) + table.add_row(['visible_message_count', queue['visible_message_count']]) + table.add_row(['tags', listing(queue['tags'] or [])]) + table.add_row(['expiration', queue['expiration']]) + table.add_row(['visibility_interval', queue['visibility_interval']]) + return table def message_table(message): - t = Table(['property', 'value']) - t.align['property'] = 'r' - t.align['value'] = 'l' + """ Returns a table with details about a message """ + table = Table(['property', 'value']) + table.align['property'] = 'r' + table.align['value'] = 'l' - t.add_row(['id', message['id']]) - t.add_row(['initial_entry_time', message['initial_entry_time']]) - t.add_row(['visibility_delay', message['visibility_delay']]) - t.add_row(['visibility_interval', message['visibility_interval']]) - t.add_row(['fields', message['fields']]) - return [t, message['body']] + table.add_row(['id', message['id']]) + table.add_row(['initial_entry_time', message['initial_entry_time']]) + table.add_row(['visibility_delay', message['visibility_delay']]) + table.add_row(['visibility_interval', message['visibility_interval']]) + table.add_row(['fields', message['fields']]) + return [table, message['body']] def topic_table(topic): - t = Table(['property', 'value']) - t.align['property'] = 'r' - t.align['value'] = 'l' + """ Returns a table with details about a topic """ + table = Table(['property', 'value']) + table.align['property'] = 'r' + table.align['value'] = 'l' - t.add_row(['name', topic['name']]) - t.add_row(['tags', listing(topic['tags'] or [])]) - return t + table.add_row(['name', topic['name']]) + table.add_row(['tags', listing(topic['tags'] or [])]) + return table def subscription_table(sub): - t = Table(['property', 'value']) - t.align['property'] = 'r' - t.align['value'] = 'l' + """ Returns a table with details about a subscription """ + table = Table(['property', 'value']) + table.align['property'] = 'r' + table.align['value'] = 'l' - t.add_row(['id', sub['id']]) - t.add_row(['endpoint_type', sub['endpoint_type']]) - for k, v in sub['endpoint'].items(): - t.add_row([k, v]) - return t + table.add_row(['id', sub['id']]) + table.add_row(['endpoint_type', sub['endpoint_type']]) + for key, val in sub['endpoint'].items(): + table.add_row([key, val]) + return table class QueueList(CLIRunnable): @@ -179,16 +184,16 @@ def execute(self, args): queues = mq_client.get_queues()['items'] - t = Table([ + table = Table([ 'name', 'message_count', 'visible_message_count' ]) for queue in queues: - t.add_row([ + table.add_row([ queue['name'], queue['message_count'], queue['visible_message_count'], ]) - return t + return table class QueueDetail(CLIRunnable): @@ -295,7 +300,7 @@ def execute(self, args): class QueuePush(CLIRunnable): __doc__ = """ -usage: sl messaging queue-push ( | [-]) +usage: sl messaging queue-push ( | -) [options] Push a message into a queue @@ -310,10 +315,10 @@ def execute(self, args): manager = MessagingManager(self.client) mq_client = manager.get_connection(args['']) body = '' - if args[''] is not None: - body = args[''] - else: + if args[''] == '-': body = sys.stdin.read() + else: + body = args[''] return message_table( mq_client.push_queue_message(args[''], body)) @@ -335,7 +340,7 @@ def execute(self, args): manager = MessagingManager(self.client) mq_client = manager.get_connection(args['']) - messages = mq_client.pop_message( + messages = mq_client.pop_messages( args[''], args.get('--count') or 1) formatted_messages = [] @@ -364,10 +369,10 @@ def execute(self, args): mq_client = manager.get_connection(args['']) topics = mq_client.get_topics()['items'] - t = Table(['name']) + table = Table(['name']) for topic in topics: - t.add_row([topic['name']]) - return t + table.add_row([topic['name']]) + return table class TopicDetail(CLIRunnable): @@ -494,7 +499,7 @@ def execute(self, args): class TopicPush(CLIRunnable): __doc__ = """ -usage: sl messaging topic-push ( | [-]) +usage: sl messaging topic-push ( | -) [options] Push a message into a topic @@ -508,9 +513,9 @@ def execute(self, args): # the message body comes from the positional argument or stdin body = '' - if args[''] is not None: - body = args[''] - else: + if args[''] == '-': body = sys.stdin.read() + else: + body = args[''] return message_table( mq_client.push_topic_message(args[''], body)) diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index 93a081030..8d0b8ccfa 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -20,7 +20,6 @@ tags Tags user_data User-defined data """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. from SoftLayer import MetadataManager, TransportError @@ -28,6 +27,8 @@ class MetaRunnable(CLIRunnable): + """ A CLIRunnable that raises a nice error on connection issues because + the metadata service is only accessable on a SoftLayer device """ def execute(self, args): try: return self._execute(args) @@ -38,6 +39,7 @@ def execute(self, args): 'network.') def _execute(self, _): + """ To be overridden exactly like the execute() method """ pass @@ -182,6 +184,7 @@ class UserMetadata(CLIRunnable): action = 'user_data' def _execute(self, _): + """ Returns user metadata """ userdata = MetadataManager().get('user_data') if userdata: return userdata @@ -200,35 +203,35 @@ class Network(MetaRunnable): def _execute(self, args): meta = MetadataManager() if args['']: - t = KeyValueTable(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' network = meta.public_network() - t.add_row([ + table.add_row([ 'mac addresses', listing(network['mac_addresses'], separator=',')]) - t.add_row([ + table.add_row([ 'router', network['router']]) - t.add_row([ + table.add_row([ 'vlans', listing(network['vlans'], separator=',')]) - t.add_row([ + table.add_row([ 'vlan ids', listing(network['vlan_ids'], separator=',')]) - return t + return table if args['']: - t = KeyValueTable(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' network = meta.private_network() - t.add_row([ + table.add_row([ 'mac addresses', listing(network['mac_addresses'], separator=',')]) - t.add_row([ + table.add_row([ 'router', network['router']]) - t.add_row([ + table.add_row([ 'vlans', listing(network['vlans'], separator=',')]) - t.add_row([ + table.add_row([ 'vlan ids', listing(network['vlan_ids'], separator=',')]) - return t + return table diff --git a/SoftLayer/CLI/modules/nas.py b/SoftLayer/CLI/modules/nas.py index 0d0fe8f5c..d252b61b6 100644 --- a/SoftLayer/CLI/modules/nas.py +++ b/SoftLayer/CLI/modules/nas.py @@ -6,7 +6,6 @@ The available commands are: list List NAS accounts """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. from SoftLayer.CLI import CLIRunnable, Table, FormattedItem @@ -26,22 +25,23 @@ class ListNAS(CLIRunnable): def execute(self, args): account = self.client['Account'] - nas = account.getNasNetworkStorage( + nas_accounts = account.getNasNetworkStorage( mask='eventCount,serviceResource[datacenter.name]') - nas = [NestedDict(n) for n in nas] + nas_accounts = [NestedDict(n) for n in nas_accounts] - t = Table(['id', 'datacenter', 'size', 'username', 'password', - 'server']) + table = Table(['id', 'datacenter', 'size', 'username', 'password', + 'server']) - for n in nas: - t.add_row([ - n['id'], - n['serviceResource']['datacenter'].get('name', blank()), + for nas_account in nas_accounts: + table.add_row([ + nas_account['id'], + nas_account['serviceResource']['datacenter'].get('name', + blank()), FormattedItem( - n.get('capacityGb', blank()), - "%dGB" % n.get('capacityGb', 0)), - n.get('username', blank()), - n.get('password', blank()), - n.get('serviceResourceBackendIpAddress', blank())]) + nas_account.get('capacityGb', blank()), + "%dGB" % nas_account.get('capacityGb', 0)), + nas_account.get('username', blank()), + nas_account.get('password', blank()), + nas_account.get('serviceResourceBackendIpAddress', blank())]) - return t + return table diff --git a/SoftLayer/CLI/modules/rwhois.py b/SoftLayer/CLI/modules/rwhois.py index fa840c607..810cf2365 100644 --- a/SoftLayer/CLI/modules/rwhois.py +++ b/SoftLayer/CLI/modules/rwhois.py @@ -7,7 +7,6 @@ edit Edit the RWhois data on the account show Show the RWhois data on the account """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. from SoftLayer import NetworkManager @@ -27,6 +26,7 @@ class RWhoisEdit(CLIRunnable): --address1=ADDR Update the address 1 field --address2=ADDR Update the address 2 field --city=CITY Set the city information + --company=NAME Set the company name --country=COUNTRY Set the country information. Use the two-letter abbreviation. --firstname=NAME Update the first name field @@ -46,6 +46,7 @@ def execute(self, args): 'abuse_email': args.get('--abuse'), 'address1': args.get('--address1'), 'address2': args.get('--address2'), + 'company_name': args.get('--company'), 'city': args.get('--city'), 'country': args.get('--country'), 'first_name': args.get('--firstname'), @@ -63,7 +64,7 @@ def execute(self, args): if not check: raise CLIAbort("You must specify at least one field to update.") - mgr.edit_rwhois(**update) + mgr.edit_rwhois(**update) # pylint: disable=W0142 class RWhoisShow(CLIRunnable): @@ -78,18 +79,18 @@ def execute(self, args): mgr = NetworkManager(self.client) result = mgr.get_rwhois() - t = KeyValueTable(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' - t.add_row(['Name', result['firstName'] + ' ' + result['lastName']]) - t.add_row(['Company', result['companyName']]) - t.add_row(['Abuse Email', result['abuseEmail']]) - t.add_row(['Address 1', result['address1']]) + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + table.add_row(['Name', result['firstName'] + ' ' + result['lastName']]) + table.add_row(['Company', result['companyName']]) + table.add_row(['Abuse Email', result['abuseEmail']]) + table.add_row(['Address 1', result['address1']]) if result.get('address2'): - t.add_row(['Address 2', result['address2']]) - t.add_row(['City', result['city']]) - t.add_row(['State', result.get('state', '-')]) - t.add_row(['Postal Code', result.get('postalCode', '-')]) - t.add_row(['Country', result['country']]) + table.add_row(['Address 2', result['address2']]) + table.add_row(['City', result['city']]) + table.add_row(['State', result.get('state', '-')]) + table.add_row(['Postal Code', result.get('postalCode', '-')]) + table.add_row(['Country', result['country']]) - return t + return table diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py index 49f77d2da..4b2b02842 100644 --- a/SoftLayer/CLI/modules/server.py +++ b/SoftLayer/CLI/modules/server.py @@ -22,7 +22,6 @@ For several commands, will be asked for. This can be the id, hostname or the ip address for a piece of hardware. """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. import re import os @@ -79,7 +78,7 @@ def execute(self, args): nic_speed=args.get('--network'), tags=tags) - t = Table([ + table = Table([ 'id', 'datacenter', 'host', @@ -89,22 +88,22 @@ def execute(self, args): 'backend_ip', 'active_transaction' ]) - t.sortby = args.get('--sortby') or 'host' + table.sortby = args.get('--sortby') or 'host' for server in servers: server = NestedDict(server) - t.add_row([ + table.add_row([ server['id'], server['datacenter']['name'] or blank(), server['fullyQualifiedDomainName'], server['processorPhysicalCoreAmount'], - gb(server['memoryCapacity']), + gb(server['memoryCapacity'] or 0), server['primaryIpAddress'] or blank(), server['primaryBackendIpAddress'] or blank(), active_txn(server), ]) - return t + return table class ServerDetails(CLIRunnable): @@ -122,27 +121,27 @@ class ServerDetails(CLIRunnable): def execute(self, args): hardware = HardwareManager(self.client) - t = KeyValueTable(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' hardware_id = resolve_id( hardware.resolve_ids, args.get(''), 'hardware') result = hardware.get_hardware(hardware_id) result = NestedDict(result) - t.add_row(['id', result['id']]) - t.add_row(['hostname', result['fullyQualifiedDomainName']]) - t.add_row(['status', result['hardwareStatus']['status']]) - t.add_row(['datacenter', result['datacenter']['name'] or blank()]) - t.add_row(['cores', result['processorPhysicalCoreAmount']]) - t.add_row(['memory', gb(result['memoryCapacity'])]) - t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) - t.add_row( + table.add_row(['id', result['id']]) + table.add_row(['hostname', result['fullyQualifiedDomainName']]) + table.add_row(['status', result['hardwareStatus']['status']]) + table.add_row(['datacenter', result['datacenter']['name'] or blank()]) + table.add_row(['cores', result['processorPhysicalCoreAmount']]) + table.add_row(['memory', gb(result['memoryCapacity'])]) + table.add_row(['public_ip', result['primaryIpAddress'] or blank()]) + table.add_row( ['private_ip', result['primaryBackendIpAddress'] or blank()]) - t.add_row(['ipmi_ip', - result['networkManagementIpAddress'] or blank()]) - t.add_row([ + table.add_row(['ipmi_ip', + result['networkManagementIpAddress'] or blank()]) + table.add_row([ 'os', FormattedItem( result['operatingSystem']['softwareLicense'] @@ -150,33 +149,34 @@ def execute(self, args): result['operatingSystem']['softwareLicense'] ['softwareDescription']['name'] or blank() )]) - t.add_row(['created', result['provisionDate'] or blank()]) + table.add_row(['created', result['provisionDate'] or blank()]) vlan_table = Table(['type', 'number', 'id']) for vlan in result['networkVlans']: vlan_table.add_row([ vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) - t.add_row(['vlans', vlan_table]) + table.add_row(['vlans', vlan_table]) if result.get('notes'): - t.add_row(['notes', result['notes']]) + table.add_row(['notes', result['notes']]) if args.get('--price'): - t.add_row(['price rate', result['billingItem']['recurringFee']]) + table.add_row(['price rate', + result['billingItem']['recurringFee']]) if args.get('--passwords'): user_strs = [] for item in result['operatingSystem']['passwords']: user_strs.append( "%s %s" % (item['username'], item['password'])) - t.add_row(['users', listing(user_strs)]) + table.add_row(['users', listing(user_strs)]) tag_row = [] for tag in result['tagReferences']: tag_row.append(tag['tag']['name']) if tag_row: - t.add_row(['tags', listing(tag_row, separator=',')]) + table.add_row(['tags', listing(tag_row, separator=',')]) if not result['privateNetworkOnlyFlag']: ptr_domains = self.client['Hardware_Server']\ @@ -184,9 +184,9 @@ def execute(self, args): for ptr_domain in ptr_domains: for ptr in ptr_domain['resourceRecords']: - t.add_row(['ptr', ptr['data']]) + table.add_row(['ptr', ptr['data']]) - return t + return table class ServerReload(CLIRunnable): @@ -237,9 +237,9 @@ class CancelServer(CLIRunnable): options = ['confirm'] def execute(self, args): - hw = HardwareManager(self.client) + mgr = HardwareManager(self.client) hw_id = resolve_id( - hw.resolve_ids, args.get(''), 'hardware') + mgr.resolve_ids, args.get(''), 'hardware') comment = args.get('--comment') @@ -249,7 +249,7 @@ def execute(self, args): reason = args.get('--reason') if args['--really'] or no_going_back(hw_id): - hw.cancel_hardware(hw_id, reason, comment) + mgr.cancel_hardware(hw_id, reason, comment) else: CLIAbort('Aborted') @@ -264,17 +264,16 @@ class ServerCancelReasons(CLIRunnable): action = 'cancel-reasons' def execute(self, args): - t = Table(['Code', 'Reason']) - t.align['Code'] = 'r' - t.align['Reason'] = 'l' + table = Table(['Code', 'Reason']) + table.align['Code'] = 'r' + table.align['Reason'] = 'l' mgr = HardwareManager(self.client) - reasons = mgr.get_cancellation_reasons().iteritems() - for code, reason in reasons: - t.add_row([code, reason]) + for code, reason in mgr.get_cancellation_reasons().items(): + table.add_row([code, reason]) - return t + return table class ServerPowerOff(CLIRunnable): @@ -287,13 +286,12 @@ class ServerPowerOff(CLIRunnable): options = ['confirm'] def execute(self, args): - hw = self.client['Hardware_Server'] mgr = HardwareManager(self.client) hw_id = resolve_id(mgr.resolve_ids, args.get(''), 'hardware') if args['--really'] or confirm('This will power off the server with ' 'id %s. Continue?' % hw_id): - hw.powerOff(id=hw_id) + self.client['Hardware_Server'].powerOff(id=hw_id) else: raise CLIAbort('Aborted.') @@ -312,18 +310,18 @@ class ServerReboot(CLIRunnable): options = ['confirm'] def execute(self, args): - hw = self.client['Hardware_Server'] + hardware_server = self.client['Hardware_Server'] mgr = HardwareManager(self.client) hw_id = resolve_id(mgr.resolve_ids, args.get(''), 'hardware') if args['--really'] or confirm('This will power off the server with ' 'id %s. Continue?' % hw_id): if args['--hard']: - hw.rebootHard(id=hw_id) + hardware_server.rebootHard(id=hw_id) elif args['--soft']: - hw.rebootSoft(id=hw_id) + hardware_server.rebootSoft(id=hw_id) else: - hw.rebootDefault(id=hw_id) + hardware_server.rebootDefault(id=hw_id) else: raise CLIAbort('Aborted.') @@ -337,11 +335,10 @@ class ServerPowerOn(CLIRunnable): action = 'power-on' def execute(self, args): - hw = self.client['Hardware_Server'] mgr = HardwareManager(self.client) hw_id = resolve_id(mgr.resolve_ids, args.get(''), 'hardware') - hw.powerOn(id=hw_id) + self.client['Hardware_Server'].powerOn(id=hw_id) class ServerPowerCycle(CLIRunnable): @@ -354,14 +351,13 @@ class ServerPowerCycle(CLIRunnable): options = ['confirm'] def execute(self, args): - hw = self.client['Hardware_Server'] mgr = HardwareManager(self.client) hw_id = resolve_id(mgr.resolve_ids, args.get(''), 'hardware') if args['--really'] or confirm('This will power off the server with ' 'id %s. Continue?' % hw_id): - hw.powerCycle(id=hw_id) + self.client['Hardware_Server'].powerCycle(id=hw_id) else: raise CLIAbort('Aborted.') @@ -398,17 +394,17 @@ class ListChassisServer(CLIRunnable): action = 'list-chassis' def execute(self, args): - t = Table(['Code', 'Chassis']) - t.align['Code'] = 'r' - t.align['Chassis'] = 'l' + table = Table(['Code', 'Chassis']) + table.align['Code'] = 'r' + table.align['Chassis'] = 'l' mgr = HardwareManager(self.client) chassis = mgr.get_available_dedicated_server_packages() for chassis in chassis: - t.add_row([chassis[0], chassis[1]]) + table.add_row([chassis[0], chassis[1]]) - return t + return table class ServerCreateOptions(CLIRunnable): @@ -436,12 +432,21 @@ class ServerCreateOptions(CLIRunnable): def execute(self, args): mgr = HardwareManager(self.client) - t = KeyValueTable(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' + table = KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' chassis_id = args.get('') + found = False + for chassis in mgr.get_available_dedicated_server_packages(): + if chassis_id == str(chassis[0]): + found = True + break + + if not found: + raise CLIAbort('Invalid chassis specified.') + ds_options = mgr.get_dedicated_server_create_options(chassis_id) show_all = True @@ -453,30 +458,50 @@ def execute(self, args): if args['--all']: show_all = True + # Determine if this is a "Bare Metal Instance" or regular server + bmc = False + if chassis_id == str(mgr.get_bare_metal_package_id()): + bmc = True + if args['--datacenter'] or show_all: results = self.get_create_options(ds_options, 'datacenter')[0] - t.add_row([results[0], listing(sorted(results[1]))]) + table.add_row([results[0], listing(sorted(results[1]))]) - if args['--cpu'] or show_all: + if (args['--cpu'] or show_all) and not bmc: results = self.get_create_options(ds_options, 'cpu') - cpu_table = Table(['id', 'description']) - for result in sorted(results): + cpu_table = Table(['ID', 'Description']) + cpu_table.align['ID'] = 'r' + cpu_table.align['Description'] = 'l' + + for result in sorted(results, key=lambda x: x[1]): cpu_table.add_row([result[1], result[0]]) - t.add_row(['cpu', cpu_table]) + table.add_row(['cpu', cpu_table]) - if args['--memory'] or show_all: + if (args['--memory'] or show_all) and not bmc: results = self.get_create_options(ds_options, 'memory')[0] - t.add_row([results[0], listing( + table.add_row([results[0], listing( item[0] for item in sorted(results[1]))]) + if bmc and (show_all or args['--memory'] or args['--cpu']): + results = self.get_create_options(ds_options, 'server_core') + memory_cpu_table = Table(['memory', 'cpu']) + for result in results: + memory_cpu_table.add_row([ + result[0], + listing( + [item[0] for item in sorted( + result[1], key=lambda x: int(x[0]) + )])]) + table.add_row(['memory/cpu', memory_cpu_table]) + if args['--os'] or show_all: results = self.get_create_options(ds_options, 'os') for result in results: - t.add_row([ + table.add_row([ result[0], listing( [item[0] for item in sorted(result[1])], @@ -486,7 +511,7 @@ def execute(self, args): if args['--disk'] or show_all: results = self.get_create_options(ds_options, 'disk')[0] - t.add_row([ + table.add_row([ results[0], listing( [item[0] for item in sorted(results[1])], @@ -497,16 +522,16 @@ def execute(self, args): results = self.get_create_options(ds_options, 'nic') for result in results: - t.add_row([result[0], listing( + table.add_row([result[0], listing( item[0] for item in sorted(result[1],))]) - if args['--controller'] or show_all: + if (args['--controller'] or show_all) and not bmc: results = self.get_create_options(ds_options, 'disk_controller')[0] - t.add_row([results[0], listing( + table.add_row([results[0], listing( item[0] for item in sorted(results[1],))]) - return t + return table def get_create_options(self, ds_options, section, pretty=True): """ This method can be used to parse the bare metal instance creation @@ -520,11 +545,13 @@ def get_create_options(self, ds_options, section, pretty=True): :param bool pretty: If true, it will return the results in a 'pretty' format that's easier to print. """ + return_value = None + if 'datacenter' == section: datacenters = [loc['keyname'] for loc in ds_options['locations']] - return [('datacenter', datacenters)] - elif 'cpu' == section: + return_value = [('datacenter', datacenters)] + elif 'cpu' == section and 'server' in ds_options['categories']: results = [] for item in ds_options['categories']['server']['items']: @@ -533,116 +560,86 @@ def get_create_options(self, ds_options, section, pretty=True): item['price_id'] )) - return results - elif 'memory' == section: + return_value = results + elif 'memory' == section and 'ram' in ds_options['categories']: ram = [] for option in ds_options['categories']['ram']['items']: ram.append((int(option['capacity']), option['price_id'])) - return [('memory', ram)] - elif 'os' == section: - os_regex = re.compile('(^[A-Za-z\s\/\-]+) ([\d\.]+)') - bit_regex = re.compile(' \((\d+)\s*bit') - extra_regex = re.compile(' - (.+)\(') - - # Encapsulate the code for generating the operating system code - def _generate_os_code(name, version, bits, extra_info): - name = name.replace(' Linux', '') - name = name.replace('Enterprise', '') - name = name.replace('GNU/Linux', '') - - os_code = name.strip().replace(' ', '_').upper() - - if os_code.startswith('RED_HAT'): - os_code = 'REDHAT' - - if 'UBUNTU' in os_code: - version = re.sub('\.\d+', '', version) - - os_code += '_' + version.replace('.0', '') - - if bits: - os_code += '_' + bits + return_value = [('memory', ram)] + elif 'server_core' == section and \ + 'server_core' in ds_options['categories']: + mem_options = {} + cpu_regex = re.compile(r'(\d+) x ') + memory_regex = re.compile(r' - (\d+) GB Ram', re.I) - if extra_info: - garbage = ['Install', '(32 bit)', '(64 bit)'] + for item in ds_options['categories']['server_core']['items']: + cpu = cpu_regex.search(item['description']).group(1) + memory = memory_regex.search(item['description']).group(1) - for g in garbage: - extra_info = extra_info.replace(g, '') + if cpu and memory: + if memory not in mem_options: + mem_options[memory] = [] - os_code += '_' + \ - extra_info.strip().replace(' ', '_').upper() + mem_options[memory].append((cpu, item['price_id'])) - return os_code - - # Also separate out the code for generating the Windows OS code - # since it's significantly different from the rest. - def _generate_windows_code(description): - version_check = re.search('Windows Server (\d+)', description) - version = version_check.group(1) - - os_code = 'WIN_' + version - - if 'Datacenter' in description: - os_code += '-DC' - elif 'Enterprise' in description: - os_code += '-ENT' - else: - os_code += '-STD' + results = [] + for memory in sorted(mem_options.keys(), key=int): + key = memory - if 'ith R2' in description: - os_code += '-R2' - elif 'ith Hyper-V' in description: - os_code += '-HYPERV' + if pretty: + key = memory - bit_check = re.search('\((\d+)\s*bit', description) - if bit_check: - os_code += '_' + bit_check.group(1) + results.append((key, mem_options[memory])) - return os_code + return_value = results + elif 'os' == section: + os_regex = re.compile(r'(^[A-Za-z\s\/\-]+) ([\d\.]+)') + bit_regex = re.compile(r' \((\d+)\s*bit') + extra_regex = re.compile(r' - (.+)\(') - # Loop through the operating systems and get their OS codes os_list = {} flat_list = [] - for os in ds_options['categories']['os']['items']: - if 'Windows Server' in os['description']: - os_code = _generate_windows_code(os['description']) + # Loop through the operating systems and get their OS codes + for opsys in ds_options['categories']['os']['items']: + if 'Windows Server' in opsys['description']: + os_code = self._generate_windows_code(opsys['description']) else: - os_results = os_regex.search(os['description']) + os_results = os_regex.search(opsys['description']) name = os_results.group(1) version = os_results.group(2) - bits = bit_regex.search(os['description']) - extra_info = extra_regex.search(os['description']) + bits = bit_regex.search(opsys['description']) + extra_info = extra_regex.search(opsys['description']) if bits: bits = bits.group(1) if extra_info: extra_info = extra_info.group(1) - os_code = _generate_os_code(name, version, bits, - extra_info) + os_code = self._generate_os_code(name, version, bits, + extra_info) name = os_code.split('_')[0] if name not in os_list: os_list[name] = [] - os_list[name].append((os_code, os['price_id'])) - flat_list.append((os_code, os['price_id'])) + os_list[name].append((os_code, opsys['price_id'])) + flat_list.append((os_code, opsys['price_id'])) if pretty: results = [] - for os in sorted(os_list.keys()): - results.append(('os (%s)' % os, os_list[os])) + for opsys in sorted(os_list.keys()): + results.append(('os (%s)' % opsys, os_list[opsys])) - return results + return_value = results else: - return [('os', flat_list)] + return_value = [('os', flat_list)] elif 'disk' == section: disks = [] - type_regex = re.compile('^[\d\.]+[GT]B\s+(.+)$') + type_regex = re.compile(r'^[\d\.]+[GT]B\s+(.+)$') for disk in ds_options['categories']['disk0']['items']: disk_type = 'SATA' disk_type = type_regex.match(disk['description']).group(1) @@ -652,7 +649,7 @@ def _generate_windows_code(description): disk_type = str(int(disk['capacity'])) + '_' + disk_type disks.append((disk_type, disk['price_id'], disk['id'])) - return [('disk', disks)] + return_value = [('disk', disks)] elif 'nic' == section: single = [] dual = [] @@ -665,7 +662,7 @@ def _generate_windows_code(description): single.append((str(int(item['capacity'])), item['price_id'])) - return [('single nic', single), ('dual nic', dual)] + return_value = [('single nic', single), ('dual nic', dual)] elif 'disk_controller' == section: options = [] for item in ds_options['categories']['disk_controller']['items']: @@ -676,7 +673,65 @@ def _generate_windows_code(description): options.append((text, item['price_id'])) - return [('disk_controllers', options)] + return_value = [('disk_controllers', options)] + + return return_value + + def _generate_os_code(self, name, version, bits, extra_info): + """ Encapsulates the code for generating the operating system code. """ + name = name.replace(' Linux', '') + name = name.replace('Enterprise', '') + name = name.replace('GNU/Linux', '') + + os_code = name.strip().replace(' ', '_').upper() + + if os_code.startswith('RED_HAT'): + os_code = 'REDHAT' + + if 'UBUNTU' in os_code: + version = re.sub(r'\.\d+', '', version) + + os_code += '_' + version.replace('.0', '') + + if bits: + os_code += '_' + bits + + if extra_info: + garbage = ['Install', '(32 bit)', '(64 bit)'] + + for obj in garbage: + extra_info = extra_info.replace(obj, '') + + os_code += '_' + extra_info.strip().replace(' ', '_').upper() + + return os_code + + def _generate_windows_code(self, description): + """ Separates the code for generating the Windows OS code + since it's significantly different from the rest. + """ + version_check = re.search(r'Windows Server (\d+)', description) + version = version_check.group(1) + + os_code = 'WIN_' + version + + if 'Datacenter' in description: + os_code += '-DC' + elif 'Enterprise' in description: + os_code += '-ENT' + else: + os_code += '-STD' + + if 'ith R2' in description: + os_code += '-R2' + elif 'ith Hyper-V' in description: + os_code += '-HYPERV' + + bit_check = re.search(r'\((\d+)\s*bit', description) + if bit_check: + os_code += '_' + bit_check.group(1) + + return os_code class CreateServer(CLIRunnable): @@ -693,26 +748,29 @@ class CreateServer(CLIRunnable): -c --cpu=CPU CPU model -o OS, --os=OS OS install code. -m --memory=MEMORY Memory in gigabytes. example: 4 - + --billing=BILLING Billing rate. Options are "monthly" (default) or + "hourly". The hourly rate is only available on the + "Bare Metal Instance" chassis. Optional: - -d, --datacenter=DC datacenter name - Note: Omitting this value defaults to the first - available datacenter - -n, --network=MBPS Network port speed in Mbps - -d, --disk=SIZE... Disks. Can be specified multiple times - --controller=RAID The RAID configuration for the server. - Defaults to None. - -k KEY, --key=KEY SSH keys to assign to the root user. Can be specified - multiple times. - --dry-run, --test Do not create the server, just get a quote - --vlan_public=VLAN The ID of the public VLAN on which you want the hardware - placed - --vlan_private=VLAN The ID of the private VLAN on which you want the - hardware placed - -t, --template=FILE A template file that defaults the command-line - options using the long name in INI format - --export=FILE Exports options to a template file + -d, --datacenter=DC Datacenter name + Note: Omitting this value defaults to the first + available datacenter + -n, --network=MBPS Network port speed in Mbps + --disk=SIZE... Disks. Can be specified multiple times + --controller=RAID The RAID configuration for the server. + Defaults to None. + -i, --postinstall=URI Post-install script to download + -k KEY, --key=KEY SSH keys to assign to the root user. Can be specified + multiple times. + --dry-run, --test Do not create the server, just get a quote + --vlan_public=VLAN The ID of the public VLAN on which you want the + hardware placed + --vlan_private=VLAN The ID of the private VLAN on which you want the + hardware placed + -t, --template=FILE A template file that defaults the command-line + options using the long name in INI format + --export=FILE Exports options to a template file """ action = 'create' options = ['confirm'] @@ -735,6 +793,64 @@ def execute(self, args): ds_options = mgr.get_dedicated_server_create_options(args['--chassis']) + order = self._process_args(args, ds_options) + + # Do not create hardware server with --test or --export + do_create = not (args['--export'] or args['--test']) + + output = None + if args.get('--test'): + result = mgr.verify_order(**order) + + table = Table(['Item', 'cost']) + table.align['Item'] = 'r' + table.align['cost'] = 'r' + + total = 0.0 + for price in result['prices']: + total += float(price.get('recurringFee', 0.0)) + rate = "%.2f" % float(price['recurringFee']) + + table.add_row([price['item']['description'], rate]) + + table.add_row(['Total monthly cost', "%.2f" % total]) + output = [] + output.append(table) + output.append(FormattedItem( + '', + ' -- ! Prices reflected here are retail and do not ' + 'take account level discounts and are not guaranteed.') + ) + + if args['--export']: + export_file = args.pop('--export') + export_to_template(export_file, args, exclude=['--wait', '--test']) + return 'Successfully exported options to a template file.' + + if do_create: + if args['--really'] or confirm( + "This action will incur charges on your account. " + "Continue?"): + result = mgr.place_order(**order) + + table = KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', result['orderId']]) + table.add_row(['created', result['orderDate']]) + output = table + else: + raise CLIAbort('Aborting dedicated server order.') + + return output + + def _process_args(self, args, ds_options): + """ + Helper method to centralize argument processing without convoluting + code flow of the main execute method. + """ + mgr = HardwareManager(self.client) + order = { 'hostname': args['--hostname'], 'domain': args['--domain'], @@ -742,6 +858,11 @@ def execute(self, args): 'package_id': args['--chassis'], } + # Determine if this is a "Bare Metal Instance" or regular server + bmc = False + if args['--chassis'] == str(mgr.get_bare_metal_package_id()): + bmc = True + # Convert the OS code back into a price ID os_price = self._get_price_id_from_options(ds_options, 'os', args['--os']) @@ -752,9 +873,19 @@ def execute(self, args): raise CLIAbort('Invalid operating system specified.') order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' - order['server'] = args['--cpu'] - order['ram'] = self._get_price_id_from_options(ds_options, 'memory', - int(args['--memory'])) + + if bmc: + order['server'] = self._get_cpu_and_memory_price_ids( + ds_options, args['--cpu'], args['--memory']) + order['bare_metal'] = True + + if args['--billing'] == 'hourly': + order['hourly'] = True + else: + order['server'] = args['--cpu'] + order['ram'] = self._get_price_id_from_options( + ds_options, 'memory', int(args['--memory'])) + # Set the disk sizes disk_prices = [] disk_number = 0 @@ -770,15 +901,16 @@ def execute(self, args): order['disks'] = disk_prices # Set the disk controller price - if args.get('--controller'): - dc_price = self._get_price_id_from_options( - ds_options, 'disk_controller', args.get('--controller')) - else: - dc_price = self._get_price_id_from_options(ds_options, - 'disk_controller', - 'None') + if not bmc: + if args.get('--controller'): + dc_price = self._get_price_id_from_options( + ds_options, 'disk_controller', args.get('--controller')) + else: + dc_price = self._get_price_id_from_options(ds_options, + 'disk_controller', + 'None') - order['disk_controller'] = dc_price + order['disk_controller'] = dc_price # Set the port speed port_speed = args.get('--network') or '100' @@ -791,6 +923,9 @@ def execute(self, args): else: raise CLIAbort('Invalid NIC speed specified.') + if args.get('--postinstall'): + order['post_uri'] = args.get('--postinstall') + # Get the SSH keys if args.get('--key'): keys = [] @@ -806,62 +941,17 @@ def execute(self, args): if args.get('--vlan_private'): order['private_vlan'] = args['--vlan_private'] - # Do not create hardware server with --test or --export - do_create = not (args['--export'] or args['--test']) - - output = None - if args.get('--test'): - result = mgr.verify_order(**order) - - t = Table(['Item', 'cost']) - t.align['Item'] = 'r' - t.align['cost'] = 'r' - - total = 0.0 - for price in result['prices']: - total += float(price.get('recurringFee', 0.0)) - rate = "%.2f" % float(price['recurringFee']) - - t.add_row([price['item']['description'], rate]) - - t.add_row(['Total monthly cost', "%.2f" % total]) - output = [] - output.append(t) - output.append(FormattedItem( - '', - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guaranteed.') - ) - - if args['--export']: - export_file = args.pop('--export') - export_to_template(export_file, args, exclude=['--wait', '--test']) - return 'Successfully exported options to a template file.' - - if do_create: - if args['--really'] or confirm( - "This action will incur charges on your account. " - "Continue?"): - result = mgr.place_order(**order) - - t = KeyValueTable(['name', 'value']) - t.align['name'] = 'r' - t.align['value'] = 'l' - t.add_row(['id', result['orderId']]) - t.add_row(['created', result['orderDate']]) - output = t - else: - raise CLIAbort('Aborting dedicated server order.') - - return output + return order def _validate_args(self, args): + """ Raises an ArgumentError if the given arguments are not valid """ invalid_args = [k for k in self.required_params if args.get(k) is None] if invalid_args: raise ArgumentError('Missing required options: %s' % ','.join(invalid_args)) def _get_default_value(self, ds_options, option): + """ Returns a 'free' price id given an option """ if option not in ds_options['categories']: return @@ -876,6 +966,7 @@ def _get_default_value(self, ds_options, option): return item['price_id'] def _get_disk_price(self, ds_options, value, number): + """ Returns a price id that matches a given disk config """ if not number: return self._get_price_id_from_options(ds_options, 'disk', value) # This will get the item ID for the matching identifier string, which @@ -888,12 +979,28 @@ def _get_disk_price(self, ds_options, value, number): if item['id'] == item_id: return item['price_id'] + def _get_cpu_and_memory_price_ids(self, ds_options, cpu_value, + memory_value): + """ + Returns a price id for a cpu/memory pair in pre-configured servers + (formerly known as BMC). + """ + ds_obj = ServerCreateOptions() + for memory, options in ds_obj.get_create_options(ds_options, + 'server_core', + False): + if memory == memory_value: + for cpu_size, price_id in options: + if cpu_size == cpu_value: + return price_id + def _get_price_id_from_options(self, ds_options, option, value, item_id=False): + """ Returns a price_id for a given option and value """ ds_obj = ServerCreateOptions() - for _, v in ds_obj.get_create_options(ds_options, option, False): - for item_options in v: + for _, options in ds_obj.get_create_options(ds_options, option, False): + for item_options in options: if item_options[0] == value: if not item_id: return item_options[1] @@ -929,17 +1036,14 @@ def execute(self, args): if args.get('--userdata'): data['userdata'] = args['--userdata'] elif args.get('--userfile'): - f = open(args['--userfile'], 'r') - try: - data['userdata'] = f.read() - finally: - f.close() + with open(args['--userfile'], 'r') as userfile: + data['userdata'] = userfile.read() data['hostname'] = args.get('--hostname') data['domain'] = args.get('--domain') - hw = HardwareManager(self.client) - hw_id = resolve_id(hw.resolve_ids, args.get(''), + mgr = HardwareManager(self.client) + hw_id = resolve_id(mgr.resolve_ids, args.get(''), 'hardware') - if not hw.edit(hw_id, **data): + if not mgr.edit(hw_id, **data): raise CLIAbort("Failed to update hardware") diff --git a/SoftLayer/CLI/modules/snapshot.py b/SoftLayer/CLI/modules/snapshot.py new file mode 100644 index 000000000..21bfeeee6 --- /dev/null +++ b/SoftLayer/CLI/modules/snapshot.py @@ -0,0 +1,153 @@ +""" +usage: sl snapshot [] [...] [options] + +Manage, order, delete iSCSI snapshots + +The available commands are: + cancel Cancel an iSCSI snapshot + create Create a snapshot of given iSCSI volume + create-space Orders space for storing snapshots + list List snpshots of given iSCSI + restore-volume Restores volume from existing snapshot + +For several commands will be asked for.This can be the id +of iSCSI volume or iSCSI snapshot. +""" +from SoftLayer.CLI import (CLIRunnable, Table) +from SoftLayer.CLI.helpers import ( + ArgumentError, NestedDict, + resolve_id) +from SoftLayer import ISCSIManager + + +class CreateSnapshot(CLIRunnable): + + """ +usage: sl snapshot create [options] + +Create a snapshot of the iSCSI volume. + +Examples: + sl snapshot create 123456 --note='Backup' + sl snapshot create 123456 + +Options: + --notes=NOTE An optional snapshot's note + +""" + action = 'create' + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + iscsi_id = resolve_id( + iscsi_mgr.resolve_ids, + args.get(''), + 'iSCSI') + notes = args.get('--notes') + iscsi_mgr.create_snapshot(iscsi_id, notes) + + +class CreateSnapshotSpace(CLIRunnable): + + """ +usage: sl snapshot create-space [options] + +Orders snapshot space for given iSCSI. + +Examples: + sl snapshot create-space 123456 --capacity=20 + +Required : + --capacity=CAPACITY Size of snapshot space to create +""" + + action = 'create-space' + required_params = ['--capacity'] + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + invalid_args = [k for k in self.required_params if args.get(k) is None] + if invalid_args: + raise ArgumentError('Missing required options: %s' + % ','.join(invalid_args)) + iscsi_id = resolve_id( + iscsi_mgr.resolve_ids, + args.get(''), + 'iSCSI') + capacity = args.get('--capacity') + iscsi_mgr.create_snapshot_space(iscsi_id, capacity) + + +class CancelSnapshot(CLIRunnable): + + """ +usage: sl snapshot cancel [options] + +Cancel/Delete iSCSI snapshot. + +""" + action = 'cancel' + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + snapshot_id = resolve_id( + iscsi_mgr.resolve_ids, + args.get(''), + 'Snapshot') + iscsi_mgr.delete_snapshot(snapshot_id) + + +class RestoreVolumeFromSnapshot(CLIRunnable): + + """ +usage: sl snapshot restore-volume + +restores volume from existing snapshot. + +""" + action = 'restore-volume' + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + volume_id = resolve_id( + iscsi_mgr.resolve_ids, args.get(''), 'iSCSI') + snapshot_id = resolve_id( + iscsi_mgr.resolve_ids, + args.get(''), + 'Snapshot') + iscsi_mgr.restore_from_snapshot(volume_id, snapshot_id) + + +class ListSnapshots(CLIRunnable): + + """ +usage: sl snapshot list + +List iSCSI Snapshots +""" + action = 'list' + + def execute(self, args): + iscsi_mgr = ISCSIManager(self.client) + iscsi_id = resolve_id( + iscsi_mgr.resolve_ids, args.get(''), 'iSCSI') + iscsi = self.client['Network_Storage_Iscsi'] + snapshots = iscsi.getPartnerships( + mask='volumeId,partnerVolumeId,createDate,type', id=iscsi_id) + snapshots = [NestedDict(n) for n in snapshots] + + table = Table([ + 'id', + 'createDate', + 'name', + 'description', + ]) + + for snapshot in snapshots: + table.add_row([ + snapshot['partnerVolumeId'], + snapshot['createDate'], + snapshot['type']['name'], + snapshot['type']['description'], + ]) + return table diff --git a/SoftLayer/CLI/modules/sshkey.py b/SoftLayer/CLI/modules/sshkey.py index b630b5449..88979191b 100644 --- a/SoftLayer/CLI/modules/sshkey.py +++ b/SoftLayer/CLI/modules/sshkey.py @@ -10,7 +10,6 @@ list Display a list of SSH keys on your account print Prints out an SSH key """ -# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: MIT, see LICENSE for more details. from os.path import expanduser @@ -45,15 +44,14 @@ def execute(self, args): if args.get('--key'): key = args['--key'] else: - f = open(expanduser(args['--file']), 'rU') - key = f.read().strip() - f.close() + key_file = open(expanduser(args['--file']), 'rU') + key = key_file.read().strip() + key_file.close() mgr = SshKeyManager(self.client) result = mgr.add_key(key, args['