diff --git a/.travis.yml b/.travis.yml index 36b5d012f..c8a48a798 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,12 @@ env: - TOX_ENV=py33 - TOX_ENV=py34 - TOX_ENV=pypy - - TOX_ENV=pep8 -install: pip install tox --use-mirrors -script: tox -e $TOX_ENV + - TOX_ENV=analysis + - TOX_ENV=coverage +install: + - pip install tox --use-mirrors + - pip install coveralls +script: + - tox -e $TOX_ENV +after_success: + - if [[ $TOX_ENV = "coverage" ]]; then coveralls; fi diff --git a/CHANGELOG b/CHANGELOG index 826acdc75..d1373a28b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,38 @@ +4.0.0 + + * Because there are many changes between version 3 and version 4, it is strongly recommend to pin the version of the SoftLayer python bindings as soon as you can in order to prevent unintentional breakage when upgrading. To keep yourself on version 3, you can use this directive: softlayer>=3,<4. That can be used with pip (pip install softlayer>=3,<4), requirements in your setup.py and/or in your requirements.txt file. + + * CLI: The command is renamed from `sl` to `slcli` to avoid package conflicts. + + * CLI: Global options now need to be specified right after the `slcli` command. For example, you would now use `sl --format=raw` vs `list over sl vs list --format=raw`. This is a change for the following options: + * --format + * -c or --config + * --debug + * --proxy + * -y or --really + * --version + + * API: The hardware manager has a significant update to how place_order() works. It will now only support the fast server provisioning package which has presets for options like CPU, Memory and disk. + + * API: The client transport is now pluggable. If you want to add extra logging or accounting, you can now subclass or wrap softlayer.transports.XmlRpcTransport in order to do so. A good example of that is done with softlayer.transports.TimingTransport. + + * API: Removed deprecated SoftLayer.CCIManager. + + * API: Adds virtual server rescue command to SoftLayer.VSManager + + * API+CLI: Adds ability to import virtual images from a given URI. The API only supports importing from a swift account using 'swift://'. For more details, see http://sldn.softlayer.com/reference/services/SoftLayer_Virtual_Guest_Block_Device_Template_Group/createFromExternalSource. + + * CLI: A `--fixtures` global flag was added to pull from fixture data instead of the API. This is useful for discovery, demonstration and testing purposes. + + * CLI: A `--verbose` or `-v` flag was added to eventually replace `--debug`. To make a command more verbose, simply add more `-v` flags. For example `sl -vvv vs list` will be the most verbose and show everything down to request/response tracing. + + * CLI: Significant changes were done to the CLI argument parsing. Docopt was dropped in favor of click. Therefore, some subtle differences which aren't documented here may exist. + + * CLI: Credentials can now be requested using `sl vs credentials `, `sl hardware credentials ` and `sl nas credentials ` for virtual servers, hardware servers and NAS accounts respectively. + + * CLI: Adds virtual server rescue command, `sl vs rescue ` + + 3.3.0 * CLI+API: Load balancer support @@ -39,13 +74,13 @@ * 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 @@ -54,21 +89,21 @@ 3.0.2 * CLI+API: Simplified object mask reformatting and added support for more complex masks. - + * CLI: Fixed the sl bmc create --network argument. - + * CLI+API: Improved output of the message queue feature and fixed some minor bugs. - + * CLI: Fixed an error when using --test and ordering a non-private subnet. - + * API: Fix to prevent double counting results in summary_by_datacenter(). - + * CLI+API: Added IPMI IP address to hardware details. - + * CLI: Added support for ordering multiple disks when creating a CCI. - + * API: Added flag to disable compression on HTTP requests. - + * CLI: Added CIDR information to subnet displays. 3.0.1 @@ -138,13 +173,13 @@ * Consistency changes/bug fixes * Added sphinx documentation. See it here: https://softlayer-api-python-client.readthedocs.org - + * CCI: Adds Support for Additional Disks - + * CCI: Adds a way to block until transactions are done on a CCI - + * CLI: For most CCI commands, you can specify id, hostname, private ip or public ip as - + * CLI: Adds the ability to filter list results for CCIs - + * API: for large result sets, requests can now be chunked into smaller batches on the server side. Using service.iter_call('getObjects', ...) or service.getObjects(..., iter=True) will return a generator regardless of the results returned. offset and limit can be passed in like normal. An additional named parameter of 'chunk' is used to limit the number of items coming back in a single request, defaults to 100 diff --git a/README.rst b/README.rst index 95ba04264..7f019fa01 100644 --- a/README.rst +++ b/README.rst @@ -2,21 +2,31 @@ SoftLayer API Python Client =========================== .. image:: https://travis-ci.org/softlayer/softlayer-python.svg?branch=master :target: https://travis-ci.org/softlayer/softlayer-python - + .. 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 -SoftLayer API bindings for Python. For use with `SoftLayer's API `_. +.. image:: https://coveralls.io/repos/softlayer/softlayer-python/badge.svg + :target: https://coveralls.io/r/softlayer/softlayer-python + +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 includes a command-line interface that can be used to manage various +SoftLayer services. -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 http://softlayer.github.io/softlayer-python/ + Installation ------------ Install via pip: @@ -29,25 +39,27 @@ Install via pip: Or you can install from source. Download source and run: .. code-block:: bash - + $ python setup.py install The most up to date version of this library can be found on the SoftLayer -GitHub public repositories: http://github.com/softlayer. Please post to the -SoftLayer forums http://forums.softlayer.com/ or open a support ticket in the -SoftLayer customer portal if you have any questions regarding use of this +GitHub public repositories at http://github.com/softlayer. Please post to the +SoftLayer forums at http://forums.softlayer.com/ or open a support ticket in +the SoftLayer customer portal if you have any questions regarding use of this library. + System Requirements ------------------- * This library has been tested on Python 2.6, 2.7, 3.3 and 3.4. -* A valid SoftLayer API username and key are required to call SoftLayer's API +* A valid SoftLayer API username and key are required to call SoftLayer's API. * A connection to SoftLayer's private network is required to connect to SoftLayer’s private network API endpoints. Copyright --------- -This software is Copyright (c) 2014 SoftLayer Technologies, Inc. +This software is Copyright (c) 2015 SoftLayer Technologies, Inc. + See the bundled LICENSE file for more information. diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 6797cf3b6..2e86aad49 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -5,7 +5,6 @@ :license: MIT, see LICENSE for more details. """ -import time import warnings from SoftLayer import auth as slauth @@ -13,12 +12,20 @@ from SoftLayer import consts from SoftLayer import transports +# pylint: disable=invalid-name + + API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT -__all__ = ['Client', 'TimedClient', 'API_PUBLIC_ENDPOINT', - 'API_PRIVATE_ENDPOINT'] - -VALID_CALL_ARGS = set([ +__all__ = [ + 'create_client_from_env', + 'Client', + 'BaseClient', + 'API_PUBLIC_ENDPOINT', + 'API_PRIVATE_ENDPOINT', +] + +VALID_CALL_ARGS = set(( 'id', 'mask', 'filter', @@ -27,11 +34,22 @@ 'raw_headers', 'limit', 'offset', -]) +)) + +def create_client_from_env(username=None, + api_key=None, + endpoint_url=None, + timeout=None, + auth=None, + config_file=None, + proxy=None, + user_agent=None, + transport=None): + """Creates a SoftLayer API client using your environment. -class Client(object): - """A SoftLayer API client. + Settings are loaded via keyword arguments, environemtal variables and + config file. :param username: an optional API username if you wish to bypass the package's built-in username @@ -47,39 +65,68 @@ class Client(object): :param config_file: A path to a configuration file used to load settings :param user_agent: an optional User Agent to report when making API calls if you wish to bypass the packages built in User Agent string + :param transport: An object that's callable with this signature: + transport(SoftLayer.transports.Request) Usage: >>> import SoftLayer - >>> client = SoftLayer.Client(username="username", api_key="api_key") + >>> client = SoftLayer.create_client_from_env() >>> resp = client['Account'].getObject() >>> resp['companyName'] 'Your Company' """ + settings = config.get_client_settings(username=username, + api_key=api_key, + endpoint_url=endpoint_url, + timeout=timeout, + proxy=proxy, + config_file=config_file) + + # Default the transport to use XMLRPC + if transport is None: + transport = transports.XmlRpcTransport( + endpoint_url=settings.get('endpoint_url'), + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + ) + + # If we have enough information to make an auth driver, let's do it + if auth is None and settings.get('username') and settings.get('api_key'): + + auth = slauth.BasicAuthentication( + settings.get('username'), + settings.get('api_key'), + ) + + return BaseClient(auth=auth, transport=transport) + + +def Client(**kwargs): + """Get a SoftLayer API Client using environmental settings. + + Deprecated in favor of create_client_from_env() + """ + warnings.warn("use SoftLayer.create_client_from_env() instead", + DeprecationWarning) + return create_client_from_env(**kwargs) + + +class BaseClient(object): + """Base SoftLayer API client. + + :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase + :param transport: An object that's callable with this signature: + transport(SoftLayer.transports.Request) + """ + _prefix = "SoftLayer_" - def __init__(self, username=None, api_key=None, endpoint_url=None, - timeout=None, auth=None, config_file=None, proxy=None, - user_agent=None): - - settings = config.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 - if settings.get('timeout'): - self.timeout = float(settings.get('timeout')) - self.proxy = None - if settings.get('proxy'): - self.proxy = settings.get('proxy') - self.user_agent = user_agent + def __init__(self, auth=None, transport=None): + self.auth = auth + self.transport = transport def authenticate_with_password(self, username, password, security_question_id=None, @@ -109,7 +156,7 @@ def __getitem__(self, name): Usage: >>> import SoftLayer - >>> client = SoftLayer.Client() + >>> client = SoftLayer.create_client_from_env() >>> client['Account'] @@ -129,7 +176,7 @@ def call(self, service, method, *args, **kwargs): Usage: >>> import SoftLayer - >>> client = SoftLayer.Client() + >>> client = SoftLayer.create_client_from_env() >>> client['Account'].getVirtualGuests(mask="id", limit=10) [...] @@ -142,13 +189,10 @@ def call(self, service, method, *args, **kwargs): raise TypeError( 'Invalid keyword arguments: %s' % ','.join(invalid_kwargs)) - if not service.startswith(self._prefix): + if self._prefix and not service.startswith(self._prefix): service = self._prefix + service - http_headers = { - 'User-Agent': self.user_agent or consts.USER_AGENT, - 'Content-Type': 'application/xml', - } + http_headers = {} if kwargs.get('compress', True): http_headers['Accept'] = '*/*' @@ -158,13 +202,10 @@ def call(self, service, method, *args, **kwargs): http_headers.update(kwargs.get('raw_headers')) request = transports.Request() - request.endpoint = self.endpoint_url request.service = service request.method = method request.args = args request.transport_headers = http_headers - request.timeout = self.timeout - request.proxy = self.proxy request.identifier = kwargs.get('id') request.mask = kwargs.get('mask') request.filter = kwargs.get('filter') @@ -181,22 +222,25 @@ def call(self, service, method, *args, **kwargs): request = self.auth.get_request(request) - return transports.make_xml_rpc_api_call(request) + return self.transport(request) __call__ = call - def iter_call(self, service, method, - chunk=100, limit=None, offset=0, *args, **kwargs): + def iter_call(self, service, method, *args, **kwargs): """A generator that deals with paginating through results. :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 integer chunk: result size for each API call (defaults to 100) :param \\*args: same optional arguments that ``Service.call`` takes :param \\*\\*kwargs: same optional keyword arguments that ``Service.call`` takes """ + chunk = kwargs.pop('chunk', 100) + limit = kwargs.pop('limit', None) + offset = kwargs.pop('offset', 0) + if chunk <= 0: raise AttributeError("Chunk size should be greater than zero.") @@ -214,6 +258,7 @@ def iter_call(self, service, method, # Don't over-fetch past the given limit if chunk + result_count > limit: chunk = limit - result_count + results = self.call(service, method, offset=offset, limit=chunk, *args, **kwargs) @@ -237,8 +282,7 @@ def iter_call(self, service, method, break def __repr__(self): - return "" % (self.endpoint_url, - self.auth) + return "Client(transport=%r, auth=%r)" % (self.transport, self.auth) __str__ = __repr__ @@ -246,40 +290,6 @@ def __len__(self): return 0 -class TimedClient(Client): - """Client that records API call timings. - - Using this class will time every call to the API and store it in an - internal list. This will have a slight impact on your client's memory - usage and performance. You should only use this for debugging. - - """ - - 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.""" - start_time = time.time() - result = super(TimedClient, self).call(service, method, *args, - **kwargs) - end_time = time.time() - diff = end_time - start_time - self.last_calls.append((service + '.' + method, start_time, diff)) - return result - - def get_last_calls(self): - """Retrieves the last_calls property. - - This property will contain a list of tuples in the form - ('SERVICE.METHOD', initiated_utc_timestamp, execution_time) - """ - last_calls = self.last_calls - self.last_calls = [] - return last_calls - - class Service(object): """A SoftLayer Service. @@ -311,7 +321,7 @@ def call(self, name, *args, **kwargs): Usage: >>> import SoftLayer - >>> client = SoftLayer.Client() + >>> client = SoftLayer.create_client_from_env() >>> client['Account'].getVirtualGuests(mask="id", limit=10) [...] @@ -331,7 +341,7 @@ def iter_call(self, name, *args, **kwargs): Usage: >>> import SoftLayer - >>> client = SoftLayer.Client() + >>> client = SoftLayer.create_client_from_env() >>> gen = client['Account'].getVirtualGuests(iter=True) >>> for virtual_guest in gen: ... virtual_guest['id'] diff --git a/SoftLayer/CLI/call_api.py b/SoftLayer/CLI/call_api.py new file mode 100644 index 000000000..e24e185d9 --- /dev/null +++ b/SoftLayer/CLI/call_api.py @@ -0,0 +1,93 @@ +"""Call arbitrary API endpoints.""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +@click.command('call', short_help="Call arbitrary API endpoints.") +@click.argument('service') +@click.argument('method') +@click.argument('parameters', nargs=-1) +@click.option('--id', '_id', help="Init parameter") +@click.option('--mask', help="String-based object mask") +@click.option('--limit', type=click.INT, help="Result limit") +@click.option('--offset', type=click.INT, help="Result offset") +@environment.pass_env +def cli(env, service, method, parameters, _id, mask, limit, offset): + """Call arbitrary API endpoints with the given SERVICE and METHOD. + + \b + Examples: + slcli call-api Account getObject + slcli call-api Account getVirtualGuests --limit=10 --mask=id,hostname + slcli call-api Virtual_Guest getObject --id=12345 + slcli call-api Metric_Tracking_Object getBandwidthData --id=1234 \\ + "2015-01-01 00:00:00" "2015-01-1 12:00:00" public + """ + result = env.client.call(service, method, *parameters, + id=_id, + mask=mask, + limit=limit, + offset=offset) + return format_api_result(result) + + +def format_api_result(value): + """Convert raw API responses to response tables.""" + if isinstance(value, list): + return format_api_list(value) + if isinstance(value, dict): + return format_api_dict(value) + return value + + +def format_api_dict(result): + """Format dictionary responses into key-value table.""" + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + for key, value in result.items(): + value = format_api_result(value) + table.add_row([key, value]) + + return table + + +def format_api_list(result): + """Format list responses into a table.""" + + if not result: + return result + + if isinstance(result[0], dict): + return format_api_list_objects(result) + + table = formatting.Table(["Value"]) + for item in result: + table.add_row([format_api_result(item)]) + return table + + +def format_api_list_objects(result): + """Format list of objects into a table.""" + + all_keys = set() + for item in result: + all_keys = all_keys.union(item.keys()) + + all_keys = sorted(all_keys) + table = formatting.Table(all_keys) + + for item in result: + values = [] + for key in all_keys: + value = format_api_result(item.get(key)) + values.append(value) + + table.add_row(values) + + return table diff --git a/SoftLayer/CLI/cdn/__init__.py b/SoftLayer/CLI/cdn/__init__.py new file mode 100644 index 000000000..678a299ea --- /dev/null +++ b/SoftLayer/CLI/cdn/__init__.py @@ -0,0 +1,2 @@ +"""Content Delivery Network.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/cdn/detail.py b/SoftLayer/CLI/cdn/detail.py new file mode 100644 index 000000000..64ccc0cbb --- /dev/null +++ b/SoftLayer/CLI/cdn/detail.py @@ -0,0 +1,32 @@ +"""Detail a CDN Account.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('account_id') +@environment.pass_env +def cli(env, account_id): + """Detail a CDN Account.""" + + manager = SoftLayer.CDNManager(env.client) + account = manager.get_account(account_id) + + table = formatting.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', formatting.blank())]) + + return table diff --git a/SoftLayer/CLI/cdn/list.py b/SoftLayer/CLI/cdn/list.py new file mode 100644 index 000000000..9cf1220bf --- /dev/null +++ b/SoftLayer/CLI/cdn/list.py @@ -0,0 +1,43 @@ +"""List CDN Accounts.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.option('--sortby', + help='Column to sort by', + type=click.Choice(['id', + 'datacenter', + 'host', + 'cores', + 'memory', + 'primary_ip', + 'backend_ip'])) +@environment.pass_env +def cli(env, sortby): + """List all CDN accounts.""" + + manager = SoftLayer.CDNManager(env.client) + accounts = manager.list_accounts() + + table = formatting.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', formatting.blank()) + ]) + + table.sortby = sortby + return table diff --git a/SoftLayer/CLI/cdn/load.py b/SoftLayer/CLI/cdn/load.py new file mode 100644 index 000000000..5dc53ca2b --- /dev/null +++ b/SoftLayer/CLI/cdn/load.py @@ -0,0 +1,18 @@ +"""Cache one or more files on all edge nodes.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('account_id') +@click.argument('content_url', nargs=-1) +@environment.pass_env +def cli(env, account_id, content_url): + """Cache one or more files on all edge nodes.""" + + manager = SoftLayer.CDNManager(env.client) + manager.load_content(account_id, content_url) diff --git a/SoftLayer/CLI/cdn/origin_add.py b/SoftLayer/CLI/cdn/origin_add.py new file mode 100644 index 000000000..c8f5635b5 --- /dev/null +++ b/SoftLayer/CLI/cdn/origin_add.py @@ -0,0 +1,24 @@ +"""Create an origin pull mapping.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('account_id') +@click.argument('content_url') +@click.option('--type', + help='The media type for this mapping (http, flash, wm, ...)', + default='http') +@click.option('--cname', + help='An optional CNAME to attach to the mapping') +@environment.pass_env +def cli(env, account_id, content_url, type, cname): + """Create an origin pull mapping.""" + + manager = SoftLayer.CDNManager(env.client) + manager.add_origin(account_id, type, content_url, cname) diff --git a/SoftLayer/CLI/cdn/origin_list.py b/SoftLayer/CLI/cdn/origin_list.py new file mode 100644 index 000000000..3a7b7c841 --- /dev/null +++ b/SoftLayer/CLI/cdn/origin_list.py @@ -0,0 +1,28 @@ +"""List origin pull mappings.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('account_id') +@environment.pass_env +def cli(env, account_id): + """List origin pull mappings.""" + + manager = SoftLayer.CDNManager(env.client) + origins = manager.get_origins(account_id) + + table = formatting.Table(['id', 'media_type', 'cname', 'origin_url']) + + for origin in origins: + table.add_row([origin['id'], + origin['mediaType'], + origin.get('cname', formatting.blank()), + origin['originUrl']]) + + return table diff --git a/SoftLayer/CLI/cdn/origin_remove.py b/SoftLayer/CLI/cdn/origin_remove.py new file mode 100644 index 000000000..055a93bb2 --- /dev/null +++ b/SoftLayer/CLI/cdn/origin_remove.py @@ -0,0 +1,18 @@ +"""Remove an origin pull mapping.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('account_id') +@click.argument('origin_id') +@environment.pass_env +def cli(env, account_id, origin_id): + """Remove an origin pull mapping.""" + + manager = SoftLayer.CDNManager(env.client) + manager.remove_origin(account_id, origin_id) diff --git a/SoftLayer/CLI/cdn/purge.py b/SoftLayer/CLI/cdn/purge.py new file mode 100644 index 000000000..09d0810ae --- /dev/null +++ b/SoftLayer/CLI/cdn/purge.py @@ -0,0 +1,18 @@ +"""Purge cached files from all edge nodes.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('account_id') +@click.argument('content_url', nargs=-1) +@environment.pass_env +def cli(env, account_id, content_url): + """Purge cached files from all edge nodes.""" + + manager = SoftLayer.CDNManager(env.client) + manager.purge_content(account_id, content_url) diff --git a/SoftLayer/CLI/config/__init__.py b/SoftLayer/CLI/config/__init__.py new file mode 100644 index 000000000..95bbe3281 --- /dev/null +++ b/SoftLayer/CLI/config/__init__.py @@ -0,0 +1,42 @@ +"""CLI configuration.""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer.CLI import formatting + + +def get_settings_from_client(client): + """Pull out settings from a SoftLayer.BaseClient instance. + + :param client: SoftLayer.BaseClient instance + """ + settings = { + 'username': '', + 'api_key': '', + 'timeout': '', + 'endpoint_url': '', + } + try: + settings['username'] = client.auth.username + settings['api_key'] = client.auth.api_key + except AttributeError: + pass + + try: + settings['timeout'] = client.transport.transport.timeout + settings['endpoint_url'] = client.transport.transport.endpoint_url + except AttributeError: + pass + + return settings + + +def config_table(settings): + """Returns a config table.""" + table = formatting.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 diff --git a/SoftLayer/CLI/config/setup.py b/SoftLayer/CLI/config/setup.py new file mode 100644 index 000000000..30ba070ab --- /dev/null +++ b/SoftLayer/CLI/config/setup.py @@ -0,0 +1,142 @@ +"""Setup CLI configuration.""" +# :license: MIT, see LICENSE for more details. +import os.path + +import SoftLayer +from SoftLayer import auth +from SoftLayer.CLI import config +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer import utils + +import click + + +def get_api_key(client, username, secret, endpoint_url=None): + """Attempts API-Key and password auth to get an API key. + + This will also generate an API key if one doesn't exist + """ + + client.endpoint_url = endpoint_url + client.auth = None + # Try to use a client with username/api key + if len(secret) == 64: + try: + client.auth = auth.BasicAuthentication(username, secret) + client['Account'].getCurrentUser() + return secret + except SoftLayer.SoftLayerAPIError as ex: + if 'invalid api token' not in ex.faultString.lower(): + raise + else: + # Try to use a client with username/password + client.authenticate_with_password(username, secret) + + user_record = client['Account'].getCurrentUser( + mask='id, apiAuthenticationKeys') + api_keys = user_record['apiAuthenticationKeys'] + if len(api_keys) == 0: + return client['User_Customer'].addApiAuthenticationKey( + id=user_record['id']) + return api_keys[0]['authenticationKey'] + + +@click.command() +@environment.pass_env +def cli(env): + """Edit configuration.""" + + username, secret, endpoint_url, timeout = get_user_input(env) + + api_key = get_api_key(env.client, username, secret, + endpoint_url=endpoint_url) + + path = '~/.softlayer' + if env.config_file: + path = env.config_file + config_path = os.path.expanduser(path) + + env.out(env.fmt(config.config_table({'username': username, + 'api_key': api_key, + 'endpoint_url': endpoint_url, + 'timeout': timeout}))) + + if not formatting.confirm('Are you sure you want to write settings ' + 'to "%s"?' % config_path, default=True): + raise exceptions.CLIAbort('Aborted.') + + # Persist the config file. Read the target config file in before + # setting the values to avoid clobbering settings + parsed_config = utils.configparser.RawConfigParser() + parsed_config.read(config_path) + try: + parsed_config.add_section('softlayer') + except utils.configparser.DuplicateSectionError: + pass + + parsed_config.set('softlayer', 'username', username) + parsed_config.set('softlayer', 'api_key', api_key) + parsed_config.set('softlayer', 'endpoint_url', endpoint_url) + + config_fd = os.fdopen(os.open(config_path, + (os.O_WRONLY | os.O_CREAT | os.O_TRUNC), + 0o600), + 'w') + try: + parsed_config.write(config_fd) + finally: + config_fd.close() + + return "Configuration Updated Successfully" + + +def get_user_input(env): + """Ask for username, secret (api_key or password) and endpoint_url.""" + + defaults = config.get_settings_from_client(env.client) + timeout = defaults['timeout'] + + # Ask for username + for _ in range(3): + username = (env.input('Username [%s]: ' % defaults['username']) or + defaults['username']) + if username: + break + else: + raise exceptions.CLIAbort('Aborted after 3 attempts') + + # Ask for 'secret' which can be api_key or their password + for _ in range(3): + secret = (env.getpass('API Key or Password [%s]: ' + % defaults['api_key']) or + defaults['api_key']) + if secret: + break + else: + raise exceptions.CLIAbort('Aborted after 3 attempts') + + # Ask for which endpoint they want to use + for _ in range(3): + endpoint_type = env.input( + 'Endpoint (public|private|custom): ') + endpoint_type = endpoint_type.lower() + if not endpoint_type: + endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT + break + if endpoint_type == 'public': + endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT + break + elif endpoint_type == 'private': + endpoint_url = SoftLayer.API_PRIVATE_ENDPOINT + break + elif endpoint_type == 'custom': + endpoint_url = env.input( + 'Endpoint URL [%s]: ' % defaults['endpoint_url'] + ) or defaults['endpoint_url'] + break + else: + raise exceptions.CLIAbort('Aborted after 3 attempts') + + return username, secret, endpoint_url, timeout diff --git a/SoftLayer/CLI/config/show.py b/SoftLayer/CLI/config/show.py new file mode 100644 index 000000000..394c5e9b3 --- /dev/null +++ b/SoftLayer/CLI/config/show.py @@ -0,0 +1,16 @@ +"""Show current CLI configuration.""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer.CLI import config +from SoftLayer.CLI import environment + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """Show current configuration.""" + + settings = config.get_settings_from_client(env.client) + return config.config_table(settings) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index c47b24a54..d22783949 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -1,262 +1,205 @@ """ -usage: sl [...] - sl help - sl help - sl [-h | --help] + SoftLayer.core + ~~~~~~~~~~~~~~ + Core for the SoftLayer CLI -SoftLayer Command-line Client {version} - -The available modules are: - -Compute: - image Manages compute and flex images - metadata Get details about this machine. Also available with 'my' and 'meta' - 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 - loadbal Load Balancer management - globalip Global IP address management - messaging Message Queue Service - rwhois RWhoIs operations - ssl Manages SSL - subnet Subnet ordering and management - vlan Manage VLANs on your account - -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 - -See 'sl help ' for more information on a specific module. - -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' + :license: MIT, see LICENSE for more details. """ -# :license: MIT, see LICENSE for more details. - -# pylint: disable=W0703 - +from __future__ import print_function import logging import sys - -import docopt +import types import SoftLayer from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting -from SoftLayer import consts +import click +# pylint: disable=too-many-public-methods, broad-except, unused-argument +# pylint: disable=redefined-builtin, super-init-not-called + +# Disable the cyclic import error. This is handled by an inline import. +# pylint: disable=cyclic-import DEBUG_LOGGING_MAP = { - '0': logging.CRITICAL, - '1': logging.WARNING, - '2': logging.INFO, - '3': logging.DEBUG + 0: logging.CRITICAL, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG } -VALID_FORMATS = ['raw', 'table', 'json'] - - -def _append_common_options(arg_doc): - """Append common options to the doc string""" - default_format = 'raw' - if sys.stdout.isatty(): - default_format = 'table' +VALID_FORMATS = ['table', 'raw', 'json'] +DEFAULT_FORMAT = 'raw' +if sys.stdout.isatty(): + DEFAULT_FORMAT = 'table' - 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 - 1=warn, 2=info, 3=debug - --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 +class CommandLoader(click.MultiCommand): + """Loads module for click.""" -class CommandParser(object): - """Helper class to parse commands. + def __init__(self, *path, **attrs): + click.MultiCommand.__init__(self, **attrs) + self.path = path - :param env: Environment instance - """ - def __init__(self, env): - self.env = env + def list_commands(self, ctx): + """Get module for click.""" + env = ctx.ensure_object(environment.Environment) + env.load() + return sorted(env.list_commands(*self.path)) - def get_main_help(self): - """Get main help text.""" - main_doc = __doc__.format(version=SoftLayer.__version__) - return _append_common_options(main_doc).strip() + def get_command(self, ctx, name): + """Get command for click.""" + env = ctx.ensure_object(environment.Environment) + env.load() - 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 _append_common_options(arg_doc).strip() + # Do alias lookup (only available for root commands) + if len(self.path) == 0: + name = env.resolve_alias(name) - 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) - arg_doc = command.__doc__ - - if 'confirm' in command.options: - arg_doc += """ -Prompt Options: - -y, --really Confirm all prompt actions -""" - - return _append_common_options(arg_doc).strip() + new_path = list(self.path) + new_path.append(name) + module = env.get_command(*new_path) + if isinstance(module, types.ModuleType): + return CommandLoader(*new_path, help=module.__doc__) + else: + return module + + +@click.group(help="SoftLayer Command-line Client", + epilog="""To use most commands your SoftLayer +username and api_key need to be configured. The easiest way to do that is to +use: 'slcli setup'""", + cls=CommandLoader, + context_settings={'help_option_names': ['-h', '--help']}) +@click.pass_context +@click.option('--format', + default=DEFAULT_FORMAT, + help="Output format", + type=click.Choice(VALID_FORMATS)) +@click.option('--config', '-C', + required=False, + default=click.get_app_dir('softlayer', force_posix=True), + help="Config file location", + type=click.Path(resolve_path=True)) +@click.option('--debug', + required=False, + default='0', + help="Sets the debug noise level", + type=click.Choice(sorted([str(key) for key + in DEBUG_LOGGING_MAP.keys()]))) +@click.option('--verbose', '-v', + help="Sets the debug noise level", + type=click.IntRange(0, 3, clamp=True), + count=True) +@click.option('--timings', + required=False, + is_flag=True, + help="Time each API call and display after results") +@click.option('--proxy', + required=False, + help="HTTP[S] proxy to be use to make API calls") +@click.option('--really', '-y', + is_flag=True, + required=False, + help="Confirm all prompt actions") +@click.option('--fixtures', + envvar='SL_FIXTURES', + is_flag=True, + required=False, + help="Use fixtures instead of actually making API calls") +@click.version_option(prog_name="slcli (SoftLayer Command-line)") +def cli(ctx, + format='table', + config=None, + debug=0, + verbose=0, + proxy=None, + really=False, + fixtures=False, + **kwargs): + """Main click CLI entry-point.""" + + # Set logging level + debug_int = int(debug) + if debug_int: + verbose = debug_int + + if verbose: + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler()) + logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG)) + + # Populate environement with client and set it as the context object + env = ctx.ensure_object(environment.Environment) + env.skip_confirmations = really + env.config_file = config + env.format = format + if env.client is None: + # Environment can be passed in explicitly. This is used for testing + if fixtures: + transport = SoftLayer.FixtureTransport() + else: + # Create SL Client + transport = SoftLayer.XmlRpcTransport() - def parse_main_args(self, args): - """Parse root arguments.""" - main_help = self.get_main_help() - arguments = docopt.docopt( - main_help, - version=consts.VERSION, - argv=args, - options_first=True) - arguments[''] = self.env.get_module_name(arguments['']) - return arguments + wrapped_transport = SoftLayer.TimingTransport(transport) + env.client = SoftLayer.create_client_from_env( + proxy=proxy, + config_file=config, + transport=wrapped_transport, + ) - def parse_module_args(self, module_name, args): - """Parse module arguments.""" - arg_doc = self.get_module_help(module_name) - arguments = docopt.docopt( - arg_doc, - version=consts.VERSION, - argv=[module_name] + args, - options_first=True) - 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.docopt(arg_doc, - version=consts.VERSION, - argv=[module_name] + args) - return command, arguments +@cli.resultcallback() +@click.pass_context +def output_result(ctx, result, timings=False, **kwargs): + """Outputs the results returned by the CLI and also outputs timings.""" - def parse(self, args): - """Parse entire tree of arguments.""" - # handle `sl ...` - main_args = self.parse_main_args(args) - module_name = main_args[''] + env = ctx.ensure_object(environment.Environment) + output = env.fmt(result) + if output: + env.out(output) - # handle `sl ...` - module_args = self.parse_module_args(module_name, main_args['']) + if timings: + timing_table = formatting.Table(['service', 'method', 'time']) - # get the command argument - command_name = module_args.get('') + calls = env.client.transport.get_last_calls() + for call, _, duration in calls: + timing_table.add_row([call.service, call.method, duration]) - # handle `sl ...` - return self.parse_command_args( - module_name, - command_name, - main_args['']) + env.err(env.fmt(timing_table)) -def main(args=sys.argv[1:], env=environment.Environment()): - """Entry point for the command-line client.""" - # Parse Top-Level Arguments +def main(): + """Main program. Catches several common errors and displays them nicely.""" exit_status = 0 - resolver = CommandParser(env) try: - command, command_args = resolver.parse(args) - - # Set logging level - debug_level = command_args.get('--debug') - if debug_level: - logger = logging.getLogger() - 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 = SoftLayer.TimedClient(**kwargs) - else: - client = SoftLayer.Client(**kwargs) - - # Do the thing - runnable = command(client=client, env=env) - data = runnable.execute(command_args) - if data: - out_format = command_args.get('--format', 'table') - if out_format not in VALID_FORMATS: - raise exceptions.ArgumentError('Invalid format "%s"' - % out_format) - output = formatting.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() - timing_table = formatting.KeyValueTable(['call', 'time']) - - for call, _, duration in api_calls: - timing_table.add_row([call, duration]) - - env.err(formatting.format_output(timing_table, fmt=out_format)) - - except exceptions.InvalidCommand as ex: - env.err(resolver.get_module_help(ex.module_name)) - if ex.command_name: - env.err('') - env.err(str(ex)) - exit_status = 1 - except exceptions.InvalidModule as ex: - env.err(resolver.get_main_help()) - if ex.module_name: - env.err('') - env.err(str(ex)) - exit_status = 1 - except docopt.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 exceptions.CLIAbort as ex: - env.err(str(ex.message)) - exit_status = ex.code - except SystemExit as ex: - exit_status = ex.code + cli.main() except SoftLayer.SoftLayerAPIError as ex: if 'invalid api token' in ex.faultString.lower(): - env.out("Authentication Failed: To update your credentials, use " - "'sl config setup'") + print("Authentication Failed: To update your credentials," + " use 'slcli config setup'") + exit_status = 1 else: - env.err(str(ex)) + print(str(ex)) exit_status = 1 except SoftLayer.SoftLayerError as ex: - env.err(str(ex)) + print(str(ex)) exit_status = 1 + except exceptions.CLIAbort as ex: + print(str(ex.message)) + exit_status = ex.code except Exception: import traceback - env.err("An unexpected error has occured:") - env.err(traceback.format_exc()) - env.err("Feel free to report this error as it is likely a bug:") - env.err(" https://github.com/softlayer/softlayer-python/issues") + print("An unexpected error has occured:") + print(str(traceback.format_exc())) + print("Feel free to report this error as it is likely a bug:") + print(" https://github.com/softlayer/softlayer-python/issues") exit_status = 1 sys.exit(exit_status) + + +if __name__ == '__main__': + main() diff --git a/SoftLayer/CLI/deprecated.py b/SoftLayer/CLI/deprecated.py new file mode 100644 index 000000000..d4c1d3140 --- /dev/null +++ b/SoftLayer/CLI/deprecated.py @@ -0,0 +1,15 @@ +""" + SoftLayer.CLI.deprecated + ~~~~~~~~~~~~~~~~~~~~~~~~ + Handles usage of the deprecated command name, 'sl'. + :license: MIT, see LICENSE for more details. +""" +from __future__ import print_function +import sys + + +def main(): + """Main function for the deprecated 'sl' command.""" + print("ERROR: Use the 'slcli' command instead.", file=sys.stderr) + print("> slcli %s" % ' '.join(sys.argv[1:]), file=sys.stderr) + exit(-1) diff --git a/SoftLayer/CLI/dns/__init__.py b/SoftLayer/CLI/dns/__init__.py new file mode 100644 index 000000000..85833186f --- /dev/null +++ b/SoftLayer/CLI/dns/__init__.py @@ -0,0 +1,2 @@ +"""Domain Name System.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/dns/record_add.py b/SoftLayer/CLI/dns/record_add.py new file mode 100644 index 000000000..856c4be53 --- /dev/null +++ b/SoftLayer/CLI/dns/record_add.py @@ -0,0 +1,27 @@ +"""Add resource record.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('zone') +@click.argument('record') +@click.argument('type') +@click.argument('data') +@click.option('--ttl', + type=click.INT, + default=7200, + help='TTL value in seconds, such as 86400') +@environment.pass_env +def cli(env, zone, record, type, data, ttl): + """Add resource record.""" + + manager = SoftLayer.DNSManager(env.client) + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + manager.create_record(zone_id, record, type, data, ttl=ttl) diff --git a/SoftLayer/CLI/dns/record_edit.py b/SoftLayer/CLI/dns/record_edit.py new file mode 100644 index 000000000..e5394e37b --- /dev/null +++ b/SoftLayer/CLI/dns/record_edit.py @@ -0,0 +1,33 @@ +"""Update DNS record.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('zone-id') +@click.option('--by-record', help='Edit by host record, such as www') +@click.option('--by-id', help='Edit a single record by its ID') +@click.option('--data', help='Record data, such as an IP address') +@click.option('--ttl', + type=click.INT, + help='TTL value in seconds, such as 86400') +@environment.pass_env +def cli(env, zone_id, by_record, by_id, data, ttl): + """Update DNS record.""" + manager = SoftLayer.DNSManager(env.client) + zone_id = helpers.resolve_id(manager.resolve_ids, zone_id, name='zone') + + results = manager.get_records(zone_id, host=by_record) + + for result in results: + if by_id and str(result['id']) != by_id: + continue + result['data'] = data or result['data'] + result['ttl'] = ttl or result['ttl'] + manager.edit_record(result) diff --git a/SoftLayer/CLI/dns/record_list.py b/SoftLayer/CLI/dns/record_list.py new file mode 100644 index 000000000..02b17db13 --- /dev/null +++ b/SoftLayer/CLI/dns/record_list.py @@ -0,0 +1,49 @@ +"""List all records in a zone.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click +# pylint: disable=redefined-builtin + + +@click.command() +@click.argument('zone') +@click.option('--data', help='Record data, such as an IP address') +@click.option('--record', help='Host record, such as www') +@click.option('--ttl', + type=click.INT, + help='TTL value in seconds, such as 86400') +@click.option('--type', help='Record type, such as A or CNAME') +@environment.pass_env +def cli(env, zone, data, record, ttl, type): + """List all records in a zone.""" + + manager = SoftLayer.DNSManager(env.client) + table = formatting.Table(['id', 'record', 'type', 'ttl', 'data']) + + table.align['ttl'] = 'l' + table.align['record'] = 'r' + table.align['data'] = 'l' + + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + + records = manager.get_records(zone_id, + record_type=type, + host=record, + ttl=ttl, + data=data) + + for record in records: + table.add_row([ + record['id'], + record['host'], + record['type'].upper(), + record['ttl'], + record['data'] + ]) + + return table diff --git a/SoftLayer/CLI/dns/record_remove.py b/SoftLayer/CLI/dns/record_remove.py new file mode 100644 index 000000000..62ec3eb70 --- /dev/null +++ b/SoftLayer/CLI/dns/record_remove.py @@ -0,0 +1,23 @@ +"""Remove resource record.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('record_id') +@environment.pass_env +def cli(env, record_id): + """Add resource record.""" + + manager = SoftLayer.DNSManager(env.client) + + if not (env.skip_confirmations or formatting.no_going_back('yes')): + raise exceptions.CLIAbort("Aborted.") + + manager.delete_record(record_id) diff --git a/SoftLayer/CLI/dns/zone_create.py b/SoftLayer/CLI/dns/zone_create.py new file mode 100644 index 000000000..21b9e9289 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_create.py @@ -0,0 +1,17 @@ +"""Create a zone.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.argument('zone') +@environment.pass_env +def cli(env, zone): + """Create a zone.""" + + manager = SoftLayer.DNSManager(env.client) + manager.create_zone(zone) diff --git a/SoftLayer/CLI/dns/zone_delete.py b/SoftLayer/CLI/dns/zone_delete.py new file mode 100644 index 000000000..3027bbed4 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_delete.py @@ -0,0 +1,25 @@ +"""Delete zone.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('zone') +@environment.pass_env +def cli(env, zone): + """Delete zone.""" + + manager = SoftLayer.DNSManager(env.client) + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + + if not (env.skip_confirmations or formatting.no_going_back(zone)): + raise exceptions.CLIAbort("Aborted.") + + manager.delete_zone(zone_id) diff --git a/SoftLayer/CLI/dns/zone_import.py b/SoftLayer/CLI/dns/zone_import.py new file mode 100644 index 000000000..505dab7d0 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_import.py @@ -0,0 +1,117 @@ +"""Import zone based off a BIND zone file.""" +# :license: MIT, see LICENSE for more details. +import re + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + +import click + +RECORD_REGEX = re.compile(r"""^((?P([\w-]+(\.)?)*|\@)?\s+ + (?P\d+)?\s+ + (?P\w+)?)?\s+ + (?P\w+)\s+ + (?P.*)""", re.X) +RECORD_FMT = "type={type}, record={record}, data={data}, ttl={ttl}" + + +@click.command() +@click.argument('zonefile', + type=click.Path(exists=True, readable=True, resolve_path=True)) +@click.option('--dry-run', is_flag=True, help="Don't actually create records") +@environment.pass_env +def cli(env, zonefile, dry_run): + """Import zone based off a BIND zone file.""" + + manager = SoftLayer.DNSManager(env.client) + with open(zonefile) as zone_f: + zone_contents = zone_f.read() + + zone, records, bad_lines = parse_zone_details(zone_contents) + + env.out("Parsed: zone=%s" % zone) + for record in records: + env.out("Parsed: %s" % RECORD_FMT.format(**record)) + + for line in bad_lines: + env.out("Unparsed: %s" % line) + + if dry_run: + return + + # Find zone id or create the zone if it doesn't exist + try: + zone_id = helpers.resolve_id(manager.resolve_ids, zone, + name='zone') + except exceptions.CLIAbort: + zone_id = manager.create_zone(zone)['id'] + env.out(click.style("Created: %s" % zone, fg='green')) + + # Attempt to create each record + for record in records: + try: + manager.create_record(zone_id, + record['record'], + record['type'], + record['data'], + record['ttl']) + + env.out(click.style("Created: %s" % RECORD_FMT.format(**record), + fg='green')) + except SoftLayer.SoftLayerAPIError as ex: + env.out(click.style("Failed: %s" % RECORD_FMT.format(**record), + fg='red')) + env.out(click.style(str(ex), fg='red')) + + env.out(click.style("Finished", fg='green')) + + +def parse_zone_details(zone_contents): + """Parses a zone file into python data-structures.""" + records = [] + bad_lines = [] + zone_lines = [line.strip() for line in zone_contents.split('\n')] + + zone_search = re.search(r'^\$ORIGIN (?P.*)\.', zone_lines[0]) + zone = zone_search.group('zone') + + for line in zone_lines[1:]: + record_search = re.search(RECORD_REGEX, line) + if record_search is None: + bad_lines.append(line) + continue + + name = record_search.group('domain') + # The API requires we send a host, although bind allows a blank + # entry. @ is the same thing as blank + if name is None: + name = "@" + + ttl = record_search.group('ttl') + # we don't do anything with the class + # domain_class = domainSearch.group('class') + record_type = record_search.group('type').upper() + data = record_search.group('data') + + # the dns class doesn't support weighted MX records yet, so we chomp + # that part out. + if record_type == "MX": + record_search = re.search(r'(?P\d+)\s+(?P.*)', data) + data = record_search.group('data') + + # This will skip the SOA record bit. And any domain that gets + # parsed oddly. + if record_type == 'IN': + bad_lines.append(line) + continue + + records.append({ + 'record': name, + 'type': record_type, + 'data': data, + 'ttl': ttl, + }) + + return zone, records, bad_lines diff --git a/SoftLayer/CLI/dns/zone_list.py b/SoftLayer/CLI/dns/zone_list.py new file mode 100644 index 000000000..8ad628af8 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_list.py @@ -0,0 +1,30 @@ +"""List all zones.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List all zones.""" + + manager = SoftLayer.DNSManager(env.client) + zones = manager.list_zones() + table = formatting.Table(['id', 'zone', 'serial', 'updated']) + table.align['serial'] = 'c' + table.align['updated'] = 'c' + + for zone in zones: + table.add_row([ + zone['id'], + zone['name'], + zone['serial'], + zone['updateDate'], + ]) + + return table diff --git a/SoftLayer/CLI/dns/zone_print.py b/SoftLayer/CLI/dns/zone_print.py new file mode 100644 index 000000000..fcb2ab0c2 --- /dev/null +++ b/SoftLayer/CLI/dns/zone_print.py @@ -0,0 +1,19 @@ +"""Print zone in BIND format.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('zone') +@environment.pass_env +def cli(env, zone): + """Print zone in BIND format.""" + + manager = SoftLayer.DNSManager(env.client) + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + return manager.dump_zone(zone_id) diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 58a024c4c..b67b8c245 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -5,119 +5,135 @@ :license: MIT, see LICENSE for more details. """ -import getpass import importlib -import inspect -import os -import os.path -import sys from SoftLayer.CLI import exceptions -from SoftLayer.CLI import modules -from SoftLayer import utils +from SoftLayer.CLI import formatting +from SoftLayer.CLI import routes -# pylint: disable=R0201 +import click +import pkg_resources + +# pylint: disable=too-many-instance-attributes, invalid-name, no-self-use class Environment(object): """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] - if None in actions: - return actions[None] - raise exceptions.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 = importlib.import_module('SoftLayer.CLI.modules.%s' - % module_name) - for _, obj in inspect.getmembers(module): - if inspect.isclass(obj) and issubclass(obj, CLIRunnable): - self.add_plugin(obj) - return module - except ImportError: - raise exceptions.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 modules.get_module_list() + # {'path:to:command': ModuleLoader()} + # {'vs:list': ModuleLoader()} + self.commands = {} + self.aliases = {} + + self.client = None + self.format = 'table' + self.skip_confirmations = False + self._modules_loaded = False + self.config_file = None def out(self, output, newline=True): """Outputs a string to the console (stdout).""" - self.stdout.write(output) - if newline: - self.stdout.write(os.linesep) + click.echo(output, nl=newline) 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) + click.echo(output, nl=newline, err=True) + + def fmt(self, output): + """Format output based on current the environment format.""" + return formatting.format_output(output, fmt=self.format) def input(self, prompt): """Provide a command prompt.""" - return utils.console_input(prompt) + return click.prompt(prompt) def getpass(self, prompt): """Provide a password prompt.""" - return getpass.getpass(prompt) + return click.prompt(prompt, hide_input=True) + + # Command loading methods + def list_commands(self, *path): + """Command listing.""" + path_str = ':'.join(path) + + commands = [] + for command in self.commands.keys(): + + # Filter based on prefix and the segment length + if all([command.startswith(path_str), + len(path) == command.count(":")]): + + # offset is used to exclude the path that the caller requested. + offset = len(path_str)+1 if path_str else 0 + commands.append(command[offset:]) + + return sorted(commands) + + def get_command(self, *path): + """Return command at the given path or raise error.""" + path_str = ':'.join(path) - def exit(self, code=0): - """Exit.""" - sys.exit(code) + if path_str in self.commands: + return self.commands[path_str].load() + raise exceptions.InvalidCommand(path) -class CLIRunnable(object): - """This represents a descrete command or action in the CLI. + def resolve_alias(self, path_str): + """Returns the actual command name. Uses the alias mapping.""" + if path_str in self.aliases: + return self.aliases[path_str] + return path_str - CLIRunnable is intended to be subclassed. + def load(self): + """Loads all modules.""" + if self._modules_loaded is True: + return - """ - options = [] # set by subclass - action = 'not set' # set by subclass + self._load_modules_from_python() + self._load_modules_from_entry_points() - def __init__(self, client=None, env=None): - self.client = client - self.env = env + self._modules_loaded = True - def execute(self, args): - """Execute the command. + def _load_modules_from_python(self): + """Load modules from the native python source.""" + for name, modpath in routes.ALL_ROUTES: + if ':' in modpath: + path, attr = modpath.split(':', 1) + else: + path, attr = modpath, None + self.commands[name] = ModuleLoader(path, attr=attr) - This is intended to be overridden in a subclass. + self.aliases = routes.ALL_ALIASES + + def _load_modules_from_entry_points(self): + """Load modules from the entry_points (slower). + + Entry points can be used to add new commands to the CLI. + + Usage: + + entry_points={'softlayer.cli': ['new-cmd = mymodule.new_cmd.cli']} """ - pass + for obj in pkg_resources.iter_entry_points(group='softlayer.cli', + name=None): + self.commands[obj.name] = obj + + +class ModuleLoader(object): + """Module loader that acts a little like an EntryPoint object.""" + + def __init__(self, import_path, attr=None): + self.import_path = import_path + self.attr = attr + + def load(self): + """load and return the module/attribute.""" + module = importlib.import_module(self.import_path) + if self.attr: + return getattr(module, self.attr) + return module + + +pass_env = click.make_pass_decorator(Environment, ensure=True) diff --git a/SoftLayer/CLI/exceptions.py b/SoftLayer/CLI/exceptions.py index b1c725ef4..378823263 100644 --- a/SoftLayer/CLI/exceptions.py +++ b/SoftLayer/CLI/exceptions.py @@ -15,6 +15,12 @@ def __init__(self, code=0, *args): super(CLIHalt, self).__init__(*args) self.code = code + def __str__(self): + return "" % (self.code, + getattr(self, 'message')) + + __repr__ = __str__ + class CLIAbort(CLIHalt): """Halt the execution of the command. Gives an exit code of 2.""" @@ -32,16 +38,6 @@ def __init__(self, msg, *args): class InvalidCommand(SoftLayer.SoftLayerError): """Raised when trying to use a command that does not exist.""" - def __init__(self, module_name, command_name, *args): - self.module_name = module_name - self.command_name = command_name - error = 'Invalid command: "%s".' % self.command_name - SoftLayer.SoftLayerError.__init__(self, error, *args) - - -class InvalidModule(SoftLayer.SoftLayerError): - """Raised when trying to use a module that does not exist.""" - def __init__(self, module_name, *args): - self.module_name = module_name - error = 'Invalid module: "%s".' % self.module_name - SoftLayer.SoftLayerError.__init__(self, error, *args) + def __init__(self, path, *args): + msg = 'Invalid command: "%s"' % ' '.join(path) + SoftLayer.SoftLayerError.__init__(self, msg, *args) diff --git a/SoftLayer/CLI/firewall/__init__.py b/SoftLayer/CLI/firewall/__init__.py new file mode 100644 index 000000000..70b92df9f --- /dev/null +++ b/SoftLayer/CLI/firewall/__init__.py @@ -0,0 +1,18 @@ +"""Firewalls.""" +# :license: MIT, see LICENSE for more details. + +from SoftLayer.CLI import exceptions + + +def parse_id(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 exceptions.CLIAbort( + 'Invalid ID %s: ID should be of the form xxx:yyy' % input_id) + return key_value[0], int(key_value[1]) diff --git a/SoftLayer/CLI/firewall/add.py b/SoftLayer/CLI/firewall/add.py new file mode 100644 index 000000000..9bf4db177 --- /dev/null +++ b/SoftLayer/CLI/firewall/add.py @@ -0,0 +1,54 @@ +"""Create new firewall.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('target') +@click.option('--firewall-type', + type=click.Choice(['vs', 'vlan', 'server']), + help='Firewall type', + required=True) +@click.option('--high-availability', '--ha', + is_flag=True, + help='High available firewall option') +@environment.pass_env +def cli(env, target, firewall_type, high_availability): + """Create new firewall.""" + + mgr = SoftLayer.FirewallManager(env.client) + + if not env.skip_confirmations: + if firewall_type == 'vlan': + pkg = mgr.get_dedicated_package(ha_enabled=high_availability) + elif firewall_type == 'vs': + pkg = mgr.get_standard_package(target, is_virt=True) + elif firewall_type == 'server': + pkg = mgr.get_standard_package(target, is_virt=False) + + if not pkg: + return "Unable to add firewall - Is network public enabled?" + + env.out("******************") + env.out("Product: %s" % pkg[0]['description']) + env.out("Price: $%s monthly" % pkg[0]['prices'][0]['recurringFee']) + env.out("******************") + + if not formatting.confirm("This action will incur charges on your " + "account. Continue?"): + raise exceptions.CLIAbort('Aborted.') + + if firewall_type == 'vlan': + mgr.add_vlan_firewall(target, ha_enabled=high_availability) + elif firewall_type == 'vs': + mgr.add_standard_firewall(target, is_virt=True) + elif firewall_type == 'server': + mgr.add_standard_firewall(target, is_virt=False) + + return "Firewall is being created!" diff --git a/SoftLayer/CLI/firewall/cancel.py b/SoftLayer/CLI/firewall/cancel.py new file mode 100644 index 000000000..2953ecff2 --- /dev/null +++ b/SoftLayer/CLI/firewall/cancel.py @@ -0,0 +1,34 @@ +"""List firewalls.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import firewall +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """List firewalls.""" + + mgr = SoftLayer.FirewallManager(env.client) + firewall_type, firewall_id = firewall.parse_id(identifier) + + if not (env.skip_confirmations or + formatting.confirm("This action will cancel a firewall from your " + "account. Continue?")): + raise exceptions.CLIAbort('Aborted.') + + if firewall_type in ['vs', 'server']: + mgr.cancel_firewall(firewall_id, dedicated=False) + elif firewall_type == 'vlan': + mgr.cancel_firewall(firewall_id, dedicated=True) + else: + raise exceptions.CLIAbort('Unknown firewall type: %s' % firewall_type) + + return 'Firewall with id %s is being cancelled!' % identifier diff --git a/SoftLayer/CLI/firewall/detail.py b/SoftLayer/CLI/firewall/detail.py new file mode 100644 index 000000000..4ba53dc62 --- /dev/null +++ b/SoftLayer/CLI/firewall/detail.py @@ -0,0 +1,49 @@ +"""Detail firewall.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import firewall +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Detail firewall.""" + + mgr = SoftLayer.FirewallManager(env.client) + + firewall_type, firewall_id = firewall.parse_id(identifier) + if firewall_type == 'vlan': + rules = mgr.get_dedicated_fwl_rules(firewall_id) + else: + rules = mgr.get_standard_fwl_rules(firewall_id) + + return get_rules_table(rules) + + +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 = formatting.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 diff --git a/SoftLayer/CLI/firewall/edit.py b/SoftLayer/CLI/firewall/edit.py new file mode 100644 index 000000000..0ea61e330 --- /dev/null +++ b/SoftLayer/CLI/firewall/edit.py @@ -0,0 +1,180 @@ +"""List firewalls.""" +# :license: MIT, see LICENSE for more details. + +import os +import subprocess +import tempfile + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import firewall +from SoftLayer.CLI import formatting + +import click + +DELIMITER = "=========================================\n" + + +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 + + +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() + subprocess.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() + subprocess.call([editor, tfile.name]) + tfile.seek(0) + data = tfile.read() + return data + + return + + +def get_formatted_rule(rule=None): + """Helper to format the rule into a user friendly format. + + :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))) + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Edit firewall rules.""" + + mgr = SoftLayer.FirewallManager(env.client) + + firewall_type, firewall_id = firewall.parse_id(identifier) + if firewall_type == '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) + env.out(edited_rules) + if formatting.confirm("Would you like to submit the rules. " + "Continue?"): + while True: + try: + rules = parse_rules(edited_rules) + if firewall_type == 'vlan': + rules = mgr.edit_dedicated_fwl_rules(firewall_id, + rules) + else: + rules = mgr.edit_standard_fwl_rules(firewall_id, + rules) + break + except (SoftLayer.SoftLayerError, ValueError) as error: + env.out("Unexpected error({%s})" % (error)) + if formatting.confirm("Would you like to continue editing " + "the rules. Continue?"): + edited_rules = open_editor(content=edited_rules) + env.out(edited_rules) + if formatting.confirm("Would you like to submit the " + "rules. Continue?"): + continue + else: + raise exceptions.CLIAbort('Aborted.') + else: + raise exceptions.CLIAbort('Aborted.') + return 'Firewall updated!' + else: + raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/firewall/list.py b/SoftLayer/CLI/firewall/list.py new file mode 100644 index 000000000..8f619a704 --- /dev/null +++ b/SoftLayer/CLI/firewall/list.py @@ -0,0 +1,85 @@ +"""List firewalls.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List firewalls.""" + + mgr = SoftLayer.FirewallManager(env.client) + table = formatting.Table(['firewall id', + 'type', + 'features', + 'server/vlan id']) + fwvlans = mgr.get_firewalls() + dedicated_firewalls = [firewall for firewall in fwvlans + if firewall['dedicatedFirewallFlag']] + + for vlan in dedicated_firewalls: + features = [] + if vlan['highAvailabilityFirewallFlag']: + features.append('HA') + + if features: + feature_list = formatting.listing(features, separator=',') + else: + feature_list = formatting.blank() + + table.add_row([ + 'vlan:%s' % vlan['networkVlanFirewall']['id'], + 'VLAN - dedicated', + feature_list, + vlan['id'] + ]) + + shared_vlan = [firewall for firewall in fwvlans + if not firewall['dedicatedFirewallFlag']] + for vlan in shared_vlan: + vs_firewalls = [guest + for guest in vlan['firewallGuestNetworkComponents'] + if has_firewall_component(guest)] + + for firewall in vs_firewalls: + table.add_row([ + 'vs:%s' % firewall['id'], + 'Virtual Server - standard', + '-', + firewall['guestNetworkComponent']['guest']['id'] + ]) + + server_firewalls = [server + for server in vlan['firewallNetworkComponents'] + if has_firewall_component(server)] + + for firewall in server_firewalls: + table.add_row([ + 'server:%s' % firewall['id'], + 'Server - standard', + '-', + utils.lookup(firewall, + 'networkComponent', + 'downlinkComponent', + 'hardwareId') + ]) + + return table + + +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 diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index 8c2578f28..1f7e495a6 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -10,6 +10,7 @@ import json import os +import click import prettytable from SoftLayer import utils @@ -170,7 +171,7 @@ def valid_response(prompt, *valid): :param string prompt: string prompt to give to the user :param string \\*valid: valid responses """ - ans = utils.console_input(prompt).lower() + ans = click.prompt(prompt).lower() if ans in valid: return True @@ -187,9 +188,9 @@ def confirm(prompt_str, default=False): :param bool default: Default value to True or False """ if default: - prompt = '%s [Y/n]: ' % prompt_str + prompt = '%s [Y/n]' % prompt_str else: - prompt = '%s [y/N]: ' % prompt_str + prompt = '%s [y/N]' % prompt_str response = valid_response(prompt, 'y', 'yes', 'yeah', 'yup', 'yolo') @@ -210,7 +211,7 @@ def no_going_back(confirmation): return valid_response( 'This action cannot be undone! ' - 'Type "%s" or press Enter to abort: ' % confirmation, + 'Type "%s" or press Enter to abort' % confirmation, str(confirmation)) @@ -251,7 +252,6 @@ def __init__(self, columns): self.columns = columns self.rows = [] self.align = {} - self.format = {} self.sortby = None def add_row(self, row): diff --git a/SoftLayer/CLI/globalip/__init__.py b/SoftLayer/CLI/globalip/__init__.py new file mode 100644 index 000000000..68fb2f949 --- /dev/null +++ b/SoftLayer/CLI/globalip/__init__.py @@ -0,0 +1,2 @@ +"""Global IP addresses.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/globalip/assign.py b/SoftLayer/CLI/globalip/assign.py new file mode 100644 index 000000000..ba13739af --- /dev/null +++ b/SoftLayer/CLI/globalip/assign.py @@ -0,0 +1,21 @@ +"""Assigns the global IP to a target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@click.argument('target') +@environment.pass_env +def cli(env, identifier, target): + """Assigns the global IP to a target.""" + + mgr = SoftLayer.NetworkManager(env.client) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, + name='global ip') + mgr.assign_global_ip(global_ip_id, target) diff --git a/SoftLayer/CLI/globalip/cancel.py b/SoftLayer/CLI/globalip/cancel.py new file mode 100644 index 000000000..7d0252d2e --- /dev/null +++ b/SoftLayer/CLI/globalip/cancel.py @@ -0,0 +1,26 @@ +"""Cancel global IP.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Cancel global IP.""" + + mgr = SoftLayer.NetworkManager(env.client) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, + name='global ip') + + if not (env.skip_confirmations or formatting.no_going_back(global_ip_id)): + raise exceptions.CLIAbort('Aborted') + + mgr.cancel_global_ip(global_ip_id) diff --git a/SoftLayer/CLI/globalip/create.py b/SoftLayer/CLI/globalip/create.py new file mode 100644 index 000000000..a95880718 --- /dev/null +++ b/SoftLayer/CLI/globalip/create.py @@ -0,0 +1,44 @@ +"""Creates a global IP.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.option('--ipv6', '--v6', is_flag=True, help='Order a IPv6 IP') +@click.option('--test', help='test order') +@environment.pass_env +def cli(env, ipv6, test): + """Creates a global IP.""" + + mgr = SoftLayer.NetworkManager(env.client) + + version = 4 + if ipv6: + version = 6 + + if not (test or env.skip_confirmations): + if not formatting.confirm("This action will incur charges on your " + "account. Continue?"): + raise exceptions.CLIAbort('Cancelling order.') + + result = mgr.add_global_ip(version=version, test_order=test) + + table = formatting.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']) + + table.add_row([price['item']['description'], rate]) + + table.add_row(['Total monthly cost', "%.2f" % total]) + return table diff --git a/SoftLayer/CLI/globalip/list.py b/SoftLayer/CLI/globalip/list.py new file mode 100644 index 000000000..d1a86bc08 --- /dev/null +++ b/SoftLayer/CLI/globalip/list.py @@ -0,0 +1,50 @@ +"""List all global IPs.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.option('--ip-version', + help='Display only IPv4', + type=click.Choice(['v4', 'v6'])) +@environment.pass_env +def cli(env, ip_version): + """List all global IPs.""" + + mgr = SoftLayer.NetworkManager(env.client) + + table = formatting.Table(['id', 'ip', 'assigned', 'target']) + + version = None + if ip_version == 'v4': + version = 4 + elif ip_version == 'v6': + version = 6 + + ips = mgr.list_global_ips(version=version) + + for ip_address in ips: + assigned = 'No' + target = 'None' + if ip_address.get('destinationIpAddress'): + dest = ip_address['destinationIpAddress'] + assigned = 'Yes' + target = dest['ipAddress'] + virtual_guest = dest.get('virtualGuest') + if virtual_guest: + target += (' (%s)' + % virtual_guest['fullyQualifiedDomainName']) + elif ip_address['destinationIpAddress'].get('hardware'): + target += (' (%s)' + % dest['hardware']['fullyQualifiedDomainName']) + + table.add_row([ip_address['id'], + ip_address['ipAddress']['ipAddress'], + assigned, + target]) + return table diff --git a/SoftLayer/CLI/globalip/unassign.py b/SoftLayer/CLI/globalip/unassign.py new file mode 100644 index 000000000..e7c0d9f7e --- /dev/null +++ b/SoftLayer/CLI/globalip/unassign.py @@ -0,0 +1,20 @@ +"""Unassigns a global IP from a target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Unassigns a global IP from a target.""" + + mgr = SoftLayer.NetworkManager(env.client) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, + name='global ip') + mgr.unassign_global_ip(global_ip_id) diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index 60d2852e9..7ee189592 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -8,6 +8,24 @@ from SoftLayer.CLI import exceptions +import click + + +def multi_option(*param_decls, **attrs): + """modify help text and indicate option is permitted multiple times + + :param param_decls: + :param attrs: + :return: + + """ + attrhelp = attrs.get('help', None) + if attrhelp is not None: + newhelp = attrhelp + " (multiple occurrence permitted)" + attrs['help'] = newhelp + attrs['multiple'] = True + return click.option(*param_decls, **attrs) + def resolve_id(resolver, identifier, name='object'): """Resolves a single id using a resolver function. @@ -30,13 +48,3 @@ def resolve_id(resolver, identifier, name='object'): (name, identifier, ', '.join([str(_id) for _id in ids]))) return ids[0] - - -def sanitize_args(args): - """ sanitize input (remove = sign from argument values) - :returns args back - """ - for key, value in args.items(): - if isinstance(value, str) and value.startswith('='): - args[key] = value[1:] - return args diff --git a/SoftLayer/CLI/image/__init__.py b/SoftLayer/CLI/image/__init__.py new file mode 100644 index 000000000..a186c27d3 --- /dev/null +++ b/SoftLayer/CLI/image/__init__.py @@ -0,0 +1,11 @@ +"""Compute images.""" +# :license: MIT, see LICENSE for more details. +from SoftLayer.CLI import formatting + + +MASK = ('id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,' + 'imageType') +DETAIL_MASK = MASK + (',children[id,blockDevicesDiskSpaceTotal,datacenter],' + 'note,createDate,status') +PUBLIC_TYPE = formatting.FormattedItem('PUBLIC', 'Public') +PRIVATE_TYPE = formatting.FormattedItem('PRIVATE', 'Private') diff --git a/SoftLayer/CLI/image/delete.py b/SoftLayer/CLI/image/delete.py new file mode 100644 index 000000000..6cfc903b7 --- /dev/null +++ b/SoftLayer/CLI/image/delete.py @@ -0,0 +1,20 @@ +"""Delete an image.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Delete an image.""" + + image_mgr = SoftLayer.ImageManager(env.client) + image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') + + image_mgr.delete_image(image_id) diff --git a/SoftLayer/CLI/image/detail.py b/SoftLayer/CLI/image/detail.py new file mode 100644 index 000000000..c716c2299 --- /dev/null +++ b/SoftLayer/CLI/image/detail.py @@ -0,0 +1,59 @@ +"""Get details for an image.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer.CLI import image as image_mod +from SoftLayer import utils + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Get details for an image.""" + + image_mgr = SoftLayer.ImageManager(env.client) + image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') + + image = image_mgr.get_image(image_id, mask=image_mod.DETAIL_MASK) + disk_space = 0 + datacenters = [] + for child in image.get('children'): + disk_space = int(child.get('blockDevicesDiskSpaceTotal', 0)) + if child.get('datacenter'): + datacenters.append(utils.lookup(child, 'datacenter', 'name')) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + table.add_row(['id', image['id']]) + table.add_row(['global_identifier', + image.get('globalIdentifier', formatting.blank())]) + table.add_row(['name', image['name'].strip()]) + table.add_row(['status', formatting.FormattedItem( + utils.lookup(image, 'status', 'keyname'), + utils.lookup(image, 'status', 'name'), + )]) + table.add_row(['account', image.get('accountId', formatting.blank())]) + table.add_row(['visibility', + image_mod.PUBLIC_TYPE if image['publicFlag'] + else image_mod.PRIVATE_TYPE]) + table.add_row(['type', + formatting.FormattedItem( + utils.lookup(image, 'imageType', 'keyName'), + utils.lookup(image, 'imageType', 'name'), + )]) + table.add_row(['flex', image.get('flexImageFlag')]) + table.add_row(['note', image.get('note')]) + table.add_row(['created', image.get('createDate')]) + table.add_row(['disk_space', formatting.b_to_gb(disk_space)]) + table.add_row(['datacenters', formatting.listing(sorted(datacenters), + separator=',')]) + + return table diff --git a/SoftLayer/CLI/image/edit.py b/SoftLayer/CLI/image/edit.py new file mode 100644 index 000000000..4f4d68892 --- /dev/null +++ b/SoftLayer/CLI/image/edit.py @@ -0,0 +1,31 @@ +"""Edit details of an image.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--name', help="Name of the image") +@click.option('--note', help="Additional note for the image") +@click.option('--tag', help="Tags for the image") +@environment.pass_env +def cli(env, identifier, name, note, tag): + """Edit details of an image.""" + + image_mgr = SoftLayer.ImageManager(env.client) + data = {} + if name: + data['name'] = name + if note: + data['note'] = note + if tag: + data['tag'] = tag + image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') + if not image_mgr.edit(image_id, **data): + raise exceptions.CLIAbort("Failed to Edit Image") diff --git a/SoftLayer/CLI/image/import.py b/SoftLayer/CLI/image/import.py new file mode 100644 index 000000000..4b47f02b9 --- /dev/null +++ b/SoftLayer/CLI/image/import.py @@ -0,0 +1,54 @@ +"""Import an image.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + + +import click + + +@click.command() +@click.argument('name') +@click.argument('uri') +@click.option('--note', default="", + help="The note to be applied to the imported template") +@click.option('--osrefcode', default="", + help="The referenceCode of the operating system software" + " description for the imported VHD") +@environment.pass_env +def cli(env, name, note, osrefcode, uri): + """Import an image.""" + + image_mgr = SoftLayer.ImageManager(env.client) + data = {} + output = [] + if name: + data['name'] = name + if note: + data['note'] = note + if osrefcode: + data['operatingSystemReferenceCode'] = osrefcode + if uri: + data['uri'] = uri + + # not sure if u should validate here or not + # if uri.endswith(".vhd") and osrefcode == "": + # raise exceptions.CLIAbort("Please specify osrefcode for .vhdImage") + + result = image_mgr.import_image_from_uri(data) + + if not result: + raise exceptions.CLIAbort("Failed to import Image") + + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['name', result['name']]) + table.add_row(['id', result['id']]) + table.add_row(['created', result['createDate']]) + table.add_row(['guid', result['globalIdentifier']]) + output.append(table) + return output diff --git a/SoftLayer/CLI/image/list.py b/SoftLayer/CLI/image/list.py new file mode 100644 index 000000000..da1494ac0 --- /dev/null +++ b/SoftLayer/CLI/image/list.py @@ -0,0 +1,55 @@ +"""List images.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import image as image_mod +from SoftLayer import utils + +import click + + +@click.command() +@click.option('--public/--private', + is_flag=True, + default=None, + help='Display only public or private images') +@environment.pass_env +def cli(env, public): + """List images.""" + + image_mgr = SoftLayer.ImageManager(env.client) + + images = [] + if public in [False, None]: + for image in image_mgr.list_private_images(mask=image_mod.MASK): + images.append(image) + + if public in [True, None]: + for image in image_mgr.list_public_images(mask=image_mod.MASK): + images.append(image) + + table = formatting.Table(['guid', + 'name', + 'type', + 'visibility', + 'account']) + + images = [image for image in images if image['parentId'] == ''] + for image in images: + + visibility = (image_mod.PUBLIC_TYPE if image['publicFlag'] + else image_mod.PRIVATE_TYPE) + table.add_row([ + image.get('globalIdentifier', formatting.blank()), + formatting.FormattedItem(image['name'], + click.wrap_text(image['name'], width=50)), + formatting.FormattedItem( + utils.lookup(image, 'imageType', 'keyName'), + utils.lookup(image, 'imageType', 'name')), + visibility, + image.get('accountId', formatting.blank()), + ]) + + return table diff --git a/SoftLayer/CLI/iscsi/__init__.py b/SoftLayer/CLI/iscsi/__init__.py new file mode 100644 index 000000000..2b379049a --- /dev/null +++ b/SoftLayer/CLI/iscsi/__init__.py @@ -0,0 +1 @@ +"""iSCSI storage.""" diff --git a/SoftLayer/CLI/iscsi/cancel.py b/SoftLayer/CLI/iscsi/cancel.py new file mode 100644 index 000000000..ff9980c30 --- /dev/null +++ b/SoftLayer/CLI/iscsi/cancel.py @@ -0,0 +1,30 @@ +"""Cancel an existing iSCSI account.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--reason', help="An optional reason for cancellation") +@click.option('--immediate', + is_flag=True, + help="Cancels the iSCSI immediately instead of on the billing " + "anniversary") +@environment.pass_env +def cli(env, identifier, reason, immediate): + """Cancel an existing iSCSI account.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, identifier, 'iSCSI') + + if not (env.skip_confirmations or formatting.no_going_back(iscsi_id)): + raise exceptions.CLIAbort('Aborted') + + iscsi_mgr.cancel_iscsi(iscsi_id, reason, immediate) diff --git a/SoftLayer/CLI/iscsi/create.py b/SoftLayer/CLI/iscsi/create.py new file mode 100644 index 000000000..0a8a480d4 --- /dev/null +++ b/SoftLayer/CLI/iscsi/create.py @@ -0,0 +1,23 @@ +"""Creates an iSCSI target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment + +import click + + +@click.command() +@click.option('--size', + type=click.INT, + required=True, + help="Size of the iSCSI volume to create (in gibibytes)") +@click.option('--datacenter', + required=True, + help="Datacenter shortname (sng01, dal05, ...)") +@environment.pass_env +def cli(env, size, datacenter): + """Creates an iSCSI target.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + iscsi_mgr.create_iscsi(size=size, location=datacenter) diff --git a/SoftLayer/CLI/iscsi/detail.py b/SoftLayer/CLI/iscsi/detail.py new file mode 100644 index 000000000..b920b41c5 --- /dev/null +++ b/SoftLayer/CLI/iscsi/detail.py @@ -0,0 +1,54 @@ +"""Get details for an iSCSI target.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer import utils + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--password', + is_flag=True, + help="Show credentials to access the iSCSI target") +@environment.pass_env +def cli(env, identifier, password): + """Get details for an iSCSI target.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + + iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, identifier, 'iSCSI') + result = iscsi_mgr.get_iscsi(iscsi_id) + result = utils.NestedDict(result) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + 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 password: + pass_table = formatting.Table(['username', 'password']) + pass_table.add_row([result['username'], result['password']]) + table.add_row(['users', pass_table]) + + return table diff --git a/SoftLayer/CLI/iscsi/list.py b/SoftLayer/CLI/iscsi/list.py new file mode 100644 index 000000000..60923ce3f --- /dev/null +++ b/SoftLayer/CLI/iscsi/list.py @@ -0,0 +1,40 @@ +"""List iSCSI targets.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List iSCSI targets.""" + + iscsi_mgr = SoftLayer.ISCSIManager(env.client) + iscsi_list = iscsi_mgr.list_iscsi() + iscsi_list = [utils.NestedDict(n) for n in iscsi_list] + table = formatting.Table([ + 'id', + 'datacenter', + 'size', + 'username', + 'password', + 'server' + ]) + for iscsi in iscsi_list: + table.add_row([ + iscsi['id'], + iscsi['serviceResource']['datacenter'].get('name', + formatting.blank()), + formatting.FormattedItem(iscsi.get('capacityGb', + formatting.blank()), + "%dGB" % iscsi.get('capacityGb', 0)), + iscsi.get('username', formatting.blank()), + iscsi.get('password', formatting.blank()), + iscsi.get('serviceResourceBackendIpAddress', + formatting.blank())]) + return table diff --git a/SoftLayer/CLI/loadbal/__init__.py b/SoftLayer/CLI/loadbal/__init__.py new file mode 100644 index 000000000..8f7becb62 --- /dev/null +++ b/SoftLayer/CLI/loadbal/__init__.py @@ -0,0 +1,12 @@ +"""Load balancers.""" + +from SoftLayer.CLI import exceptions + + +def parse_id(input_id): + """Parse the load balancer kind and actual id from the "kind:id" form.""" + parts = input_id.split(':') + if len(parts) != 2: + raise exceptions.CLIAbort( + 'Invalid ID %s: ID should be of the form "kind:id"' % input_id) + return parts[0], int(parts[1]) diff --git a/SoftLayer/CLI/loadbal/cancel.py b/SoftLayer/CLI/loadbal/cancel.py new file mode 100644 index 000000000..e7439f76d --- /dev/null +++ b/SoftLayer/CLI/loadbal/cancel.py @@ -0,0 +1,29 @@ +"""Cancel an existing load balancer.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Cancel an existing load balancer.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, loadbal_id = loadbal.parse_id(identifier) + + if not (env.skip_confirmations or + formatting.confirm("This action will cancel a load balancer. " + "Continue?")): + raise exceptions.CLIAbort('Aborted.') + + mgr.cancel_lb(loadbal_id) + return 'Load Balancer with id %s is being cancelled!' % identifier diff --git a/SoftLayer/CLI/loadbal/create.py b/SoftLayer/CLI/loadbal/create.py new file mode 100644 index 000000000..e253d62c7 --- /dev/null +++ b/SoftLayer/CLI/loadbal/create.py @@ -0,0 +1,25 @@ +"""Adds a load balancer given the id returned from create-options.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@click.argument('billing-id') +@click.option('--datacenter', '-d', + help='Datacenter shortname (sng01, dal05, ...)') +@environment.pass_env +def cli(env, billing_id, datacenter): + """Adds a load balancer given the id returned from create-options.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + if not formatting.confirm("This action will incur charges on your " + "account. Continue?"): + raise exceptions.CLIAbort('Aborted.') + mgr.add_local_lb(billing_id, datacenter=datacenter) + return "Load balancer is being created!" diff --git a/SoftLayer/CLI/loadbal/create_options.py b/SoftLayer/CLI/loadbal/create_options.py new file mode 100644 index 000000000..386256fb9 --- /dev/null +++ b/SoftLayer/CLI/loadbal/create_options.py @@ -0,0 +1,35 @@ +"""Show load balancer options.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """Reset connections on a certain service group.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + table = formatting.Table(['price_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'], + '%.2f' % float(package['prices'][0]['recurringFee']) + ]) + + return table diff --git a/SoftLayer/CLI/loadbal/detail.py b/SoftLayer/CLI/loadbal/detail.py new file mode 100644 index 000000000..9e3a817ec --- /dev/null +++ b/SoftLayer/CLI/loadbal/detail.py @@ -0,0 +1,85 @@ +"""Get Load balancer details.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Get Load balancer details.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, loadbal_id = loadbal.parse_id(identifier) + + load_balancer = mgr.get_local_lb(loadbal_id) + + table = formatting.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 = formatting.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 = formatting.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 diff --git a/SoftLayer/CLI/loadbal/group_add.py b/SoftLayer/CLI/loadbal/group_add.py new file mode 100644 index 000000000..76456f2b3 --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_add.py @@ -0,0 +1,41 @@ +"""Adds a new load_balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--allocation', + required=True, + type=click.INT, + help="The allocated percent of connections") +@click.option('--port', + required=True, + help="The port number", + type=click.INT) +@click.option('--routing-type', + required=True, + help="The port routing type") +@click.option('--routing-method', + required=True, + help="The routing method") +@environment.pass_env +def cli(env, identifier, allocation, port, routing_type, routing_method): + """Adds a new load_balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, loadbal_id = loadbal.parse_id(identifier) + + mgr.add_service_group(loadbal_id, + allocation=allocation, + port=port, + routing_type=routing_type, + routing_method=routing_method) + + return 'Load balancer service group is being added!' diff --git a/SoftLayer/CLI/loadbal/group_delete.py b/SoftLayer/CLI/loadbal/group_delete.py new file mode 100644 index 000000000..e217caa97 --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_delete.py @@ -0,0 +1,28 @@ +"""Deletes an existing load balancer service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Deletes an existing load balancer service group.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + _, group_id = loadbal.parse_id(identifier) + + if not (env.skip_confirmations or + formatting.confirm("This action will cancel a service group. " + "Continue?")): + raise exceptions.CLIAbort('Aborted.') + + mgr.delete_service_group(group_id) + return 'Service group %s is being deleted!' % identifier diff --git a/SoftLayer/CLI/loadbal/group_edit.py b/SoftLayer/CLI/loadbal/group_edit.py new file mode 100644 index 000000000..6de786284 --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_edit.py @@ -0,0 +1,41 @@ +"""Edit an existing load balancer service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--allocation', + type=click.INT, + help="Change the allocated percent of connections") +@click.option('--port', + help="Change the port number", + type=click.INT) +@click.option('--routing-type', + help="Change the port routing type") +@click.option('--routing-method', + help="Change the routing method") +@environment.pass_env +def cli(env, identifier, allocation, port, routing_type, routing_method): + """Edit an existing load balancer service group.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, group_id = loadbal.parse_id(identifier) + + # check if any input is provided + if not any([allocation, port, routing_type, routing_method]): + return 'At least one property is required to be changed!' + + mgr.edit_service_group(loadbal_id, + group_id, + allocation=allocation, + port=port, + routing_type=routing_type, + routing_method=routing_method) + + return 'Load balancer service group %s is being updated!' % identifier diff --git a/SoftLayer/CLI/loadbal/group_reset.py b/SoftLayer/CLI/loadbal/group_reset.py new file mode 100644 index 000000000..78b227d4e --- /dev/null +++ b/SoftLayer/CLI/loadbal/group_reset.py @@ -0,0 +1,21 @@ +"""Reset connections on a certain service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Reset connections on a certain service group.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, group_id = loadbal.parse_id(identifier) + + mgr.reset_service_group(loadbal_id, group_id) + return 'Load balancer service group connections are being reset!' diff --git a/SoftLayer/CLI/loadbal/health_checks.py b/SoftLayer/CLI/loadbal/health_checks.py new file mode 100644 index 000000000..7f73e0b3a --- /dev/null +++ b/SoftLayer/CLI/loadbal/health_checks.py @@ -0,0 +1,26 @@ +"""List health check types.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List health check types.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + hc_types = mgr.get_hc_types() + table = formatting.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 diff --git a/SoftLayer/CLI/loadbal/list.py b/SoftLayer/CLI/loadbal/list.py new file mode 100644 index 000000000..a462ef9b1 --- /dev/null +++ b/SoftLayer/CLI/loadbal/list.py @@ -0,0 +1,49 @@ +"""List active load balancers.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List active load balancers.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + load_balancers = mgr.get_local_lbs() + + table = formatting.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 diff --git a/SoftLayer/CLI/loadbal/routing_methods.py b/SoftLayer/CLI/loadbal/routing_methods.py new file mode 100644 index 000000000..4549e5ece --- /dev/null +++ b/SoftLayer/CLI/loadbal/routing_methods.py @@ -0,0 +1,25 @@ +"""List routing methods.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List routing types.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + routing_methods = mgr.get_routing_methods() + table = formatting.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 diff --git a/SoftLayer/CLI/loadbal/routing_types.py b/SoftLayer/CLI/loadbal/routing_types.py new file mode 100644 index 000000000..b3b76060c --- /dev/null +++ b/SoftLayer/CLI/loadbal/routing_types.py @@ -0,0 +1,24 @@ +"""List routing types.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + + +@click.command() +@environment.pass_env +def cli(env): + """List routing types.""" + mgr = SoftLayer.LoadBalancerManager(env.client) + + routing_methods = mgr.get_routing_methods() + table = formatting.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 diff --git a/SoftLayer/CLI/loadbal/service_add.py b/SoftLayer/CLI/loadbal/service_add.py new file mode 100644 index 000000000..5a9fa982a --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_add.py @@ -0,0 +1,52 @@ +"""Adds a new load balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--enabled / --disabled', + required=True, + help="Create the service as enable or disabled") +@click.option('--port', + required=True, + help="The port number for the service", + type=click.INT) +@click.option('--weight', + required=True, + type=click.INT, + help="The weight of the service") +@click.option('--healthcheck-type', + required=True, + help="The health check type") +@click.option('--ip-address', '--ip', + required=True, + help="The IP of the service") +@environment.pass_env +def cli(env, identifier, enabled, port, weight, healthcheck_type, ip_address): + """Adds a new load balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, group_id = loadbal.parse_id(identifier) + + # check if the IP is valid + ip_address_id = None + if ip_address: + ip_service = env.client['Network_Subnet_IpAddress'] + ip_record = ip_service.getByIpAddress(ip_address) + ip_address_id = ip_record['id'] + + mgr.add_service(loadbal_id, + group_id, + ip_address_id=ip_address_id, + enabled=enabled, + port=port, + weight=weight, + hc_type=healthcheck_type) + return 'Load balancer service is being added!' diff --git a/SoftLayer/CLI/loadbal/service_delete.py b/SoftLayer/CLI/loadbal/service_delete.py new file mode 100644 index 000000000..927bc86bd --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_delete.py @@ -0,0 +1,28 @@ +"""Deletes an existing load balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Deletes an existing load balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + _, service_id = loadbal.parse_id(identifier) + + if not (env.skip_confirmations or + formatting.confirm("This action will cancel a service from your " + "load balancer. Continue?")): + raise exceptions.CLIAbort('Aborted.') + + mgr.delete_service(service_id) + return 'Load balancer service %s is being cancelled!' % service_id diff --git a/SoftLayer/CLI/loadbal/service_edit.py b/SoftLayer/CLI/loadbal/service_edit.py new file mode 100644 index 000000000..0ae024edc --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_edit.py @@ -0,0 +1,49 @@ +"""Edit the properties of a service group.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@click.option('--enabled / --disabled', + default=None, + help="Enable or disable the service") +@click.option('--port', + help="Change the port number for the service", type=click.INT) +@click.option('--weight', + type=click.INT, + help="Change the weight of the service") +@click.option('--healthcheck-type', help="Change the health check type") +@click.option('--ip-address', '--ip', help="Change the IP of the service") +@environment.pass_env +def cli(env, identifier, enabled, port, weight, healthcheck_type, ip_address): + """Edit the properties of a service group.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + + loadbal_id, service_id = loadbal.parse_id(identifier) + + # check if any input is provided + if not any([ip_address, enabled, weight, port, healthcheck_type]): + return 'At least one property is required to be changed!' + + # check if the IP is valid + ip_address_id = None + if ip_address: + ip_service = env.client['Network_Subnet_IpAddress'] + ip_record = ip_service.getByIpAddress(ip_address) + ip_address_id = ip_record['id'] + + mgr.edit_service(loadbal_id, + service_id, + ip_address_id=ip_address_id, + enabled=enabled, + port=port, + weight=weight, + hc_type=healthcheck_type) + return 'Load balancer service %s is being modified!' % identifier diff --git a/SoftLayer/CLI/loadbal/service_toggle.py b/SoftLayer/CLI/loadbal/service_toggle.py new file mode 100644 index 000000000..4bd0f834f --- /dev/null +++ b/SoftLayer/CLI/loadbal/service_toggle.py @@ -0,0 +1,28 @@ +"""Toggle the status of an existing load balancer service.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import loadbal + +import click + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Toggle the status of an existing load balancer service.""" + + mgr = SoftLayer.LoadBalancerManager(env.client) + _, service_id = loadbal.parse_id(identifier) + + if not (env.skip_confirmations or + formatting.confirm("This action will toggle the status on the " + "service. Continue?")): + raise exceptions.CLIAbort('Aborted.') + + mgr.toggle_service_status(service_id) + return 'Load balancer service %s status updated!' % identifier diff --git a/SoftLayer/CLI/metadata.py b/SoftLayer/CLI/metadata.py new file mode 100644 index 000000000..d7b8a8703 --- /dev/null +++ b/SoftLayer/CLI/metadata.py @@ -0,0 +1,84 @@ +"""Find details about this machine.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +import click + +META_CHOICES = [ + 'backend_ip', + 'backend_mac', + 'datacenter', + 'datacenter_id', + 'fqdn', + 'frontend_mac', + 'id', + 'ip', + 'network', + 'provision_state', + 'tags', + 'user_data', +] + +META_MAPPING = { + 'backend_ip': 'primary_backend_ip', + 'ip': 'primary_ip', +} + +HELP = """Find details about this machine + +\b +PROP Choices +%s +\b +Examples : +%s +""" % ('*'+'\n*'.join(META_CHOICES), + 'slcli metadata '+'\nslcli metadata '.join(META_CHOICES)) + + +@click.command(help=HELP, + short_help="Find details about this machine.", + epilog="These commands only work on devices on the backend " + "SoftLayer network. This allows for self-discovery for " + "newly provisioned resources.") +@click.argument('prop', type=click.Choice(META_CHOICES)) +def cli(prop): + """Find details about this machine.""" + + try: + if prop == 'network': + return get_network() + + meta_prop = META_MAPPING.get(prop) or prop + return SoftLayer.MetadataManager().get(meta_prop) + except SoftLayer.TransportError: + raise exceptions.CLIAbort( + 'Cannot connect to the backend service address. Make sure ' + 'this command is being ran from a device on the backend ' + 'network.') + + +def get_network(): + """Returns a list of tables with public and private network details.""" + meta = SoftLayer.MetadataManager() + network_tables = [] + for network_func in [meta.public_network, meta.private_network]: + network = network_func() + + table = formatting.KeyValueTable(['name', 'value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + table.add_row(['mac addresses', + formatting.listing(network['mac_addresses'], + separator=',')]) + table.add_row(['router', network['router']]) + table.add_row(['vlans', + formatting.listing(network['vlans'], separator=',')]) + table.add_row(['vlan ids', + formatting.listing(network['vlan_ids'], separator=',')]) + network_tables.append(table) + + return network_tables diff --git a/SoftLayer/CLI/modules/__init__.py b/SoftLayer/CLI/modules/__init__.py deleted file mode 100644 index eaad02f96..000000000 --- a/SoftLayer/CLI/modules/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" - SoftLayer.CLI.modules - ~~~~~~~~~~~~~~~~~~~~~ - Contains all plugable modules for the CLI interface - - :license: MIT, see LICENSE for more details. -""" - -import pkgutil - - -def get_module_list(): - """Returns each module under SoftLayer.CLI.modules.""" - actions = [action[1] for action in pkgutil.iter_modules(__path__)] - return actions diff --git a/SoftLayer/CLI/modules/cdn.py b/SoftLayer/CLI/modules/cdn.py deleted file mode 100644 index 11f0e8b47..000000000 --- a/SoftLayer/CLI/modules/cdn.py +++ /dev/null @@ -1,188 +0,0 @@ -""" -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. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -class ListAccounts(environment.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 = SoftLayer.CDNManager(self.client) - accounts = manager.list_accounts() - - table = formatting.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', formatting.blank()) - ]) - - table.sortby = args['--sortby'] - return table - - -class DetailAccount(environment.CLIRunnable): - """ -usage: sl cdn detail [options] - -Show CDN account details -""" - action = 'detail' - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - account = manager.get_account(args.get('')) - - table = formatting.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', formatting.blank())]) - - return table - - -class LoadContent(environment.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 = SoftLayer.CDNManager(self.client) - manager.load_content(args.get(''), args.get('')) - - -class PurgeContent(environment.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 = SoftLayer.CDNManager(self.client) - manager.purge_content(args.get(''), - args.get('')) - - -class ListOrigins(environment.CLIRunnable): - """ -usage: sl cdn origin-list [options] - -List origin pull mappings associated with a CDN account. -""" - action = 'origin-list' - - def execute(self, args): - manager = SoftLayer.CDNManager(self.client) - origins = manager.get_origins(args.get('')) - - table = formatting.Table(['id', 'media_type', 'cname', 'origin_url']) - - for origin in origins: - table.add_row([origin['id'], - origin['mediaType'], - origin.get('cname', formatting.blank()), - origin['originUrl']]) - - return table - - -class AddOrigin(environment.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 = SoftLayer.CDNManager(self.client) - media_type = args.get('--type') or 'http' - - manager.add_origin(args.get(''), media_type, - args.get(''), args.get('--cname', None)) - - -class RemoveOrigin(environment.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 = SoftLayer.CDNManager(self.client) - manager.remove_origin(args.get(''), - args.get('')) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py deleted file mode 100644 index 3f84e1052..000000000 --- a/SoftLayer/CLI/modules/config.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -usage: sl config [] [...] [options] - -View and edit configuration - -The available commands are: - setup Setup configuration - show Show current configuration -""" -# :license: MIT, see LICENSE for more details. - -import os.path - -import SoftLayer -from SoftLayer import auth -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer import utils - - -def get_settings_from_client(client): - """ Pull out settings from a SoftLayer.Client instance. - - :param client: SoftLayer.Client instance - """ - settings = { - 'username': '', - 'api_key': '', - 'timeout': client.timeout or '', - 'endpoint_url': client.endpoint_url, - } - try: - settings['username'] = client.auth.username - settings['api_key'] = client.auth.api_key - except AttributeError: - pass - - return settings - - -def config_table(settings): - """ Returns a config table """ - table = formatting.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(client, username, secret, endpoint_url=None): - """ Attempts API-Key and password auth to get an API key - - This will also generate an API key if one doesn't exist - """ - - client.endpoint_url = endpoint_url - client.auth = None - # Try to use a client with username/api key - if len(secret) == 64: - try: - client.auth = auth.BasicAuthentication(username, secret) - client['Account'].getCurrentUser() - return secret - except SoftLayer.SoftLayerAPIError as ex: - if 'invalid api token' not in ex.faultString.lower(): - raise - else: - # Try to use a client with username/password - client.authenticate_with_password(username, secret) - - user_record = client['Account'].getCurrentUser( - mask='id, apiAuthenticationKeys') - api_keys = user_record['apiAuthenticationKeys'] - if len(api_keys) == 0: - return client['User_Customer'].addApiAuthenticationKey( - id=user_record['id']) - return api_keys[0]['authenticationKey'] - - -class Setup(environment.CLIRunnable): - """ -usage: sl config setup [options] - -Setup configuration -""" - action = 'setup' - - def execute(self, args): - username, secret, endpoint_url, timeout = self.get_user_input() - - api_key = get_api_key(self.client, username, secret, - endpoint_url=endpoint_url) - - path = '~/.softlayer' - if args.get('--config'): - path = args.get('--config') - config_path = os.path.expanduser(path) - - self.env.out( - formatting.format_output(config_table({ - 'username': username, - 'api_key': api_key, - 'endpoint_url': endpoint_url, - 'timeout': timeout}))) - - if not formatting.confirm('Are you sure you want to write settings ' - 'to "%s"?' % config_path, default=True): - raise exceptions.CLIAbort('Aborted.') - - # Persist the config file. Read the target config file in before - # setting the values to avoid clobbering settings - config = utils.configparser.RawConfigParser() - config.read(config_path) - try: - config.add_section('softlayer') - except utils.configparser.DuplicateSectionError: - pass - - config.set('softlayer', 'username', username) - config.set('softlayer', 'api_key', api_key) - config.set('softlayer', 'endpoint_url', endpoint_url) - - config_file = os.fdopen(os.open(config_path, - (os.O_WRONLY | os.O_CREAT), - 0o600), - 'w') - try: - config.write(config_file) - finally: - config_file.close() - - return "Configuration Updated Successfully" - - def get_user_input(self): - """ Ask for username, secret (api_key or password) and endpoint_url """ - - defaults = get_settings_from_client(self.client) - timeout = defaults['timeout'] - - # Ask for username - for _ in range(3): - username = (self.env.input('Username [%s]: ' - % defaults['username']) - or defaults['username']) - if username: - break - else: - raise exceptions.CLIAbort('Aborted after 3 attempts') - - # Ask for 'secret' which can be api_key or their password - for _ in range(3): - secret = (self.env.getpass('API Key or Password [%s]: ' - % defaults['api_key']) - or defaults['api_key']) - if secret: - break - else: - raise exceptions.CLIAbort('Aborted after 3 attempts') - - # Ask for which endpoint they want to use - for _ in range(3): - endpoint_type = self.env.input( - 'Endpoint (public|private|custom): ') - endpoint_type = endpoint_type.lower() - if not endpoint_type: - endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT - break - if endpoint_type == 'public': - endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT - break - elif endpoint_type == 'private': - endpoint_url = SoftLayer.API_PRIVATE_ENDPOINT - break - elif endpoint_type == 'custom': - endpoint_url = self.env.input( - 'Endpoint URL [%s]: ' % defaults['endpoint_url'] - ) or defaults['endpoint_url'] - break - else: - raise exceptions.CLIAbort('Aborted after 3 attempts') - - return username, secret, endpoint_url, timeout - - -class Show(environment.CLIRunnable): - """ -usage: sl config show [options] - -Show current configuration -""" - action = 'show' - - def execute(self, args): - settings = get_settings_from_client(self.client) - return config_table(settings) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py deleted file mode 100755 index 90a995b87..000000000 --- a/SoftLayer/CLI/modules/dns.py +++ /dev/null @@ -1,369 +0,0 @@ -""" -usage: sl dns [] [...] [options] - -Manage DNS - -The available zone commands are: - create Create zone - delete Delete zone - list List zones or a zone's records - print Print zone in BIND format - -The available record commands are: - add Add resource record - edit Update resource records (bulk/single) - remove Remove resource records -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - -import re - -RECORD_REGEX = re.compile(r"""^((?P([\w-]+(\.)?)*|\@)?\s+ - (?P\d+)?\s+ - (?P\w+)?)?\s+ - (?P\w+)\s+ - (?P.*)""", re.X) - - -class DumpZone(environment.CLIRunnable): - """ -usage: sl dns print [options] - -print zone in BIND format - -Arguments: - Zone name (softlayer.com) -""" - action = "print" - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - return manager.dump_zone(zone_id) - - -class CreateZone(environment.CLIRunnable): - """ -usage: sl dns create [options] - -Create a zone - -Arguments: - Zone name (softlayer.com) -""" - action = 'create' - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - manager.create_zone(args['']) - - -class DeleteZone(environment.CLIRunnable): - """ -usage: sl dns delete [options] - -Delete zone - -Arguments: - Zone name (softlayer.com) -""" - action = 'delete' - options = ['confirm'] - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - if args['--really'] or formatting.no_going_back(args['']): - manager.delete_zone(zone_id) - else: - raise exceptions.CLIAbort("Aborted.") - - -class ImportZone(environment.CLIRunnable): - """ -usage: sl dns import [options] - -Creates a new zone based off a nicely BIND formatted file - -Arguments: - Path to the bind zone file you want to import -Options: - --dry-run Don't actually do anything. This will show you what is parsed - - """ - action = 'import' - - def execute(self, args): - - dry_run = args.get('--dry-run') - - manager = SoftLayer.DNSManager(self.client) - with open(args['']) as zone_file: - zone_contents = zone_file.read() - - zone, records, bad_lines = parse_zone_details(zone_contents) - - self.env.out("Parsed: zone=%s" % zone) - for record in records: - self.env.out("Parsed: %s" % record) - for line in bad_lines: - self.env.out("Unparsed: %s" % line) - - if dry_run: - return - - # Find zone id or create the zone if it doesn't exist - try: - zone_id = helpers.resolve_id(manager.resolve_ids, zone, - name='zone') - except exceptions.CLIAbort: - zone_id = manager.create_zone(zone)['id'] - self.env.out("\033[92mCREATED ZONE: %s\033[0m" % zone) - - # Attempt to create each record - for record in records: - try: - manager.create_record(zone_id, - record['record'], - record['record_type'], - record['data'], - record['ttl']) - self.env.out("\033[92mCreated: Host: %s\033[0m" % record) - except SoftLayer.SoftLayerAPIError as ex: - self.env.out("\033[91mFAILED: %s" % record) - self.env.out("%s \033[0m" % ex) - - return "Finished" - - -def parse_zone_details(zone_contents): - """Parses a zone file into python data-structures""" - records = [] - bad_lines = [] - zone_lines = [line.strip() for line in zone_contents.split('\n')] - - zone_search = re.search(r'^\$ORIGIN (?P.*)\.', zone_lines[0]) - zone = zone_search.group('zone') - - for line in zone_lines[1:]: - record_search = re.search(RECORD_REGEX, line) - if record_search is None: - bad_lines.append(line) - continue - - name = record_search.group('domain') - # The API requires we send a host, although bind allows a blank - # entry. @ is the same thing as blank - if name is None: - name = "@" - - ttl = record_search.group('ttl') - # we don't do anything with the class - # domain_class = domainSearch.group('class') - record_type = record_search.group('type').upper() - data = record_search.group('data') - - # the dns class doesn't support weighted MX records yet, so we chomp - # that part out. - if record_type == "MX": - record_search = re.search(r'(?P\d+)\s+(?P.*)', data) - data = record_search.group('data') - - # This will skip the SOA record bit. And any domain that gets - # parsed oddly. - if record_type == 'IN': - bad_lines.append(line) - continue - - records.append({ - 'record': name, - 'record_type': record_type, - 'data': data, - 'ttl': ttl, - }) - - return zone, records, bad_lines - - -class ListZones(environment.CLIRunnable): - """ -usage: sl dns list [] [options] - -List zones and optionally, records - -Filters: - --data=DATA Record data, such as an IP address - --record=HOST Host record, such as www - --ttl=TTL TTL value in seconds, such as 86400 - --type=TYPE Record type, such as A or CNAME -""" - action = 'list' - - def execute(self, args): - if args['']: - return self.list_zone(args) - - return self.list_all_zones() - - def list_zone(self, args): - """ list records for a particular zone """ - manager = SoftLayer.DNSManager(self.client) - table = formatting.Table(['id', 'record', 'type', 'ttl', 'value']) - - table.align['ttl'] = 'l' - table.align['record'] = 'r' - table.align['value'] = 'l' - - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - records = manager.get_records( - zone_id, - record_type=args.get('--type'), - host=args.get('--record'), - ttl=args.get('--ttl'), - data=args.get('--data'), - ) - - for record in records: - table.add_row([ - record['id'], - record['host'], - record['type'].upper(), - record['ttl'], - record['data'] - ]) - - return table - - def list_all_zones(self): - """ List all zones """ - manager = SoftLayer.DNSManager(self.client) - zones = manager.list_zones() - table = formatting.Table(['id', 'zone', 'serial', 'updated']) - table.align['serial'] = 'c' - table.align['updated'] = 'c' - - for zone in zones: - table.add_row([ - zone['id'], - zone['name'], - zone['serial'], - zone['updateDate'], - ]) - - return table - - -class AddRecord(environment.CLIRunnable): - """ -usage: sl dns add [--ttl=TTL] [options] - -Add resource record - -Arguments: - Zone name (softlayer.com) - Resource record (www) - Record type. [Options: A, AAAA, - CNAME, MX, NS, PTR, SPF, SRV, TXT] - Record data. NOTE: only minor validation is done - -Options: - --ttl=TTL Time to live -""" - action = 'add' - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - manager.create_record( - zone_id, - args[''], - args[''], - args[''], - ttl=args['--ttl'] or 7200) - - -class EditRecord(environment.CLIRunnable): - """ -usage: sl dns edit [--data=DATA] [--ttl=TTL] [--id=ID] - [options] - -Update resource records (bulk/single) - -Arguments: - Zone name (softlayer.com) - Resource record (www) - -Options: - --data=DATA - --id=ID Modify only the given ID - --ttl=TTL Time to live -""" - action = 'edit' - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - results = manager.get_records( - zone_id, - host=args['']) - - for result in results: - if args['--id'] and str(result['id']) != args['--id']: - continue - result['data'] = args['--data'] or result['data'] - result['ttl'] = args['--ttl'] or result['ttl'] - manager.edit_record(result) - - -class RecordRemove(environment.CLIRunnable): - """ -usage: sl dns remove [--id=ID] [options] - -Remove resource records - -Arguments: - Zone name (softlayer.com) - Resource record (www) - -Options: - --id=ID Remove only the given ID -""" - action = 'remove' - options = ['confirm'] - - def execute(self, args): - manager = SoftLayer.DNSManager(self.client) - zone_id = helpers.resolve_id(manager.resolve_ids, args[''], - name='zone') - - if args['--id']: - records = [{'id': args['--id']}] - else: - records = manager.get_records( - zone_id, - host=args['']) - - if args['--really'] or formatting.no_going_back('yes'): - table = formatting.Table(['record']) - for result in records: - manager.delete_record(result['id']) - table.add_row([result['id']]) - - return table - raise exceptions.CLIAbort("Aborted.") diff --git a/SoftLayer/CLI/modules/filters.py b/SoftLayer/CLI/modules/filters.py deleted file mode 100644 index ae8073a6d..000000000 --- a/SoftLayer/CLI/modules/filters.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -usage: sl help filters - -Filters are used to limit the amount of results. Some commands will accept a -filter operation for certain fields. Filters can be applied across multiple -fields in most cases. - -Available Operations: - Case Insensitive - 'value' Exact value match - 'value*' Begins with value - '*value' Ends with value - '*value*' Contains value - - Case Sensitive - '~ value' Exact value match - '> value' Greater than value - '< value' Less than value - '>= value' Greater than or equal to value - '<= value' Less than or equal to value - -Examples: - sl server list --datacenter=dal05 - sl server list --hostname='prod*' - 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. -""" -# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py deleted file mode 100755 index 489cf829a..000000000 --- a/SoftLayer/CLI/modules/firewall.py +++ /dev/null @@ -1,424 +0,0 @@ -""" -usage: sl firewall [] [...] [options] - -Firewall rule and security management - -The available commands are: - 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 - -""" -# :license: MIT, see LICENSE for more details. - -from __future__ import print_function -import os -import subprocess -import tempfile - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - -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 exceptions.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 = formatting.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() - subprocess.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() - subprocess.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(environment.CLIRunnable): - """ -usage: sl firewall list [options] - -List active firewalls -""" - action = 'list' - - def execute(self, args): - mgr = SoftLayer.FirewallManager(self.client) - table = formatting.Table(['firewall id', - 'type', - 'features', - 'server/vlan id']) - fwvlans = mgr.get_firewalls() - dedicated_firewalls = [firewall for firewall in fwvlans - if firewall['dedicatedFirewallFlag']] - - for vlan in dedicated_firewalls: - features = [] - if vlan['highAvailabilityFirewallFlag']: - features.append('HA') - - if features: - feature_list = formatting.listing(features, separator=',') - else: - feature_list = formatting.blank() - - table.add_row([ - 'vlan:%s' % vlan['networkVlanFirewall']['id'], - 'VLAN - dedicated', - feature_list, - vlan['id'] - ]) - - shared_vlan = [firewall for firewall in fwvlans - if not firewall['dedicatedFirewallFlag']] - for vlan in shared_vlan: - vs_firewalls = [guest - for guest in vlan['firewallGuestNetworkComponents'] - if has_firewall_component(guest)] - - for firewall in vs_firewalls: - table.add_row([ - 'cci:%s' % firewall['id'], - 'CCI - standard', - '-', - firewall['guestNetworkComponent']['guest']['id'] - ]) - - server_firewalls = [server - for server in vlan['firewallNetworkComponents'] - if has_firewall_component(server)] - - for firewall in server_firewalls: - table.add_row([ - 'server:%s' % firewall['id'], - 'Server - standard', - '-', - utils.lookup(firewall, - 'networkComponent', - 'downlinkComponent', - 'hardwareId') - ]) - - return table - - -class FWCancel(environment.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 = SoftLayer.FirewallManager(self.client) - input_id = args.get('') - key_value = get_ids(input_id) - firewall_id = int(key_value[1]) - - if args['--really'] or formatting.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 exceptions.CLIAbort('Aborted.') - - -class FWAdd(environment.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 = SoftLayer.FirewallManager(self.client) - input_id = helpers.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 formatting.confirm("This action will incur charges on your " - "account. Continue?"): - raise exceptions.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(environment.CLIRunnable): - """ -usage: sl firewall detail [options] - -Get firewall details -""" - action = 'detail' - - def execute(self, args): - mgr = SoftLayer.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(environment.CLIRunnable): - """ -usage: sl firewall edit [options] - -Edit the rules for a firewall -""" - action = 'edit' - - def execute(self, args): - mgr = SoftLayer.FirewallManager(self.client) - input_id = args.get('') - - 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 formatting.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 (SoftLayer.SoftLayerError, ValueError) as error: - print("Unexpected error({%s})" % (error)) - if formatting.confirm("Would you like to continue editing " - "the rules. Continue?"): - edited_rules = open_editor(content=edited_rules) - print(edited_rules) - if formatting.confirm("Would you like to submit the " - "rules. Continue?"): - continue - else: - raise exceptions.CLIAbort('Aborted.') - else: - raise exceptions.CLIAbort('Aborted.') - return 'Firewall updated!' - else: - raise exceptions.CLIAbort('Aborted.') diff --git a/SoftLayer/CLI/modules/globalip.py b/SoftLayer/CLI/modules/globalip.py deleted file mode 100644 index 6f87c2a10..000000000 --- a/SoftLayer/CLI/modules/globalip.py +++ /dev/null @@ -1,170 +0,0 @@ -""" -usage: sl globalip [] [...] [options] - -Orders or configures global IP addresses - -The available commands are: - assign Assign a target to a global IP address - cancel Cancels a global IP - create Orders a new global IP address - list Display a list of global IP addresses - unassign Unassigns a global IP -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - - -class GlobalIpAssign(environment.CLIRunnable): - """ -usage: sl globalip assign [options] - -Assigns a global IP to a target. - -Required: - The ID or address of the global IP - The IP address to assign to the global IP -""" - action = 'assign' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, - args.get(''), - name='global ip') - mgr.assign_global_ip(global_ip_id, args['']) - - -class GlobalIpCancel(environment.CLIRunnable): - """ -usage: sl globalip cancel [options] - -Cancel a subnet -""" - - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, - args.get(''), - name='global ip') - - if args['--really'] or formatting.no_going_back(global_ip_id): - mgr.cancel_global_ip(global_ip_id) - else: - raise exceptions.CLIAbort('Aborted') - - -class GlobalIpCreate(environment.CLIRunnable): - """ -usage: - sl globalip create [options] - -Add a new global IP address to your account. - -Options: - --v6 Orders IPv6 - --test Do not order the IP; just get a quote -""" - action = 'create' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - - version = 4 - if args.get('--v6'): - version = 6 - if not args.get('--test') and not args['--really']: - if not formatting.confirm("This action will incur charges on your " - "account. Continue?"): - raise exceptions.CLIAbort('Cancelling order.') - result = mgr.add_global_ip(version=version, - test_order=args.get('--test')) - - table = formatting.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']) - - table.add_row([price['item']['description'], rate]) - - table.add_row(['Total monthly cost', "%.2f" % total]) - return table - - -class GlobalIpList(environment.CLIRunnable): - """ -usage: sl globalip list [options] - -Displays a list of global IPs - -Filters: - --v4 Display only IPV4 - --v6 Display only IPV6 -""" - action = 'list' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - - table = formatting.Table(['id', 'ip', 'assigned', 'target']) - table.sortby = args.get('--sortby') or 'id' - - version = 0 - if args.get('--v4'): - version = 4 - elif args.get('--v6'): - version = 6 - - ips = mgr.list_global_ips(version=version) - - for ip_address in ips: - assigned = 'No' - target = 'None' - if ip_address.get('destinationIpAddress'): - dest = ip_address['destinationIpAddress'] - assigned = 'Yes' - target = dest['ipAddress'] - virtual_guest = dest.get('virtualGuest') - if virtual_guest: - target += (' (%s)' - % virtual_guest['fullyQualifiedDomainName']) - elif ip_address['destinationIpAddress'].get('hardware'): - target += (' (%s)' - % dest['hardware']['fullyQualifiedDomainName']) - - table.add_row([ip_address['id'], - ip_address['ipAddress']['ipAddress'], - assigned, - target]) - return table - - -class GlobalIpUnassign(environment.CLIRunnable): - """ -usage: sl globalip unassign [options] - -Unassigns a global IP from a target. - -Required: - The ID or address of the global IP -""" - action = 'unassign' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, - args.get(''), - name='global ip') - mgr.unassign_global_ip(global_ip_id) diff --git a/SoftLayer/CLI/modules/help.py b/SoftLayer/CLI/modules/help.py deleted file mode 100644 index 0866687e1..000000000 --- a/SoftLayer/CLI/modules/help.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -usage: sl help [options] - sl help [options] - sl help [options] - -View help on a module or command. -""" -# :license: MIT, see LICENSE for more details. -# Missing docstrings ignored due to __doc__ = __doc__ magic -# pylint: disable=C0111 - -from SoftLayer.CLI import core -from SoftLayer.CLI import environment - - -class Show(environment.CLIRunnable): - # Use the same documentation as the module - __doc__ = __doc__ - action = None - - def execute(self, args): - parser = core.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['']) diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py deleted file mode 100644 index 5075eed60..000000000 --- a/SoftLayer/CLI/modules/image.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -usage: sl image [] [...] [options] - -Manage compute images - -The available commands are: - delete Delete an image - detail Output details about an image - list List images - edit Edit an image -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - -MASK = ('id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,' - 'imageType') -DETAIL_MASK = MASK + (',children[id,blockDevicesDiskSpaceTotal,datacenter],' - 'note,createDate,status') -PUBLIC_TYPE = formatting.FormattedItem('PUBLIC', 'Public') -PRIVATE_TYPE = formatting.FormattedItem('PRIVATE', 'Private') - - -class ListImages(environment.CLIRunnable): - """ -usage: sl image list [--public | --private] [options] - -List images - -Options: - --private Display only private images - --public Display only public images -""" - action = 'list' - - def execute(self, args): - image_mgr = SoftLayer.ImageManager(self.client) - - neither = not any([args['--private'], args['--public']]) - - images = [] - if args['--private'] or neither: - for image in image_mgr.list_private_images(mask=MASK): - images.append(image) - - if args['--public'] or neither: - for image in image_mgr.list_public_images(mask=MASK): - images.append(image) - - table = formatting.Table(['id', - 'account', - 'name', - 'type', - 'visibility', - 'global_identifier']) - - images = [image for image in images if image['parentId'] == ''] - for image in images: - - table.add_row([ - image['id'], - image.get('accountId', formatting.blank()), - image['name'].strip(), - formatting.FormattedItem( - utils.lookup(image, 'imageType', 'keyName'), - utils.lookup(image, 'imageType', 'name')), - PUBLIC_TYPE if image['publicFlag'] else PRIVATE_TYPE, - image.get('globalIdentifier', formatting.blank()), - ]) - - return table - - -class DetailImage(environment.CLIRunnable): - """ -usage: sl image detail [options] - -Get details for an image -""" - action = 'detail' - - def execute(self, args): - image_mgr = SoftLayer.ImageManager(self.client) - image_id = helpers.resolve_id(image_mgr.resolve_ids, - args.get(''), - 'image') - - image = image_mgr.get_image(image_id, mask=DETAIL_MASK) - disk_space = 0 - datacenters = [] - for child in image.get('children'): - disk_space = int(child.get('blockDevicesDiskSpaceTotal', 0)) - if child.get('datacenter'): - datacenters.append(utils.lookup(child, 'datacenter', 'name')) - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - table.add_row(['id', image['id']]) - table.add_row(['global_identifier', - image.get('globalIdentifier', formatting.blank())]) - table.add_row(['name', image['name'].strip()]) - table.add_row(['status', formatting.FormattedItem( - utils.lookup(image, 'status', 'keyname'), - utils.lookup(image, 'status', 'name'), - )]) - table.add_row(['account', image.get('accountId', formatting.blank())]) - table.add_row(['visibility', - PUBLIC_TYPE if image['publicFlag'] else PRIVATE_TYPE]) - table.add_row(['type', - formatting.FormattedItem( - utils.lookup(image, 'imageType', 'keyName'), - utils.lookup(image, 'imageType', 'name'), - )]) - table.add_row(['flex', image.get('flexImageFlag')]) - table.add_row(['note', image.get('note')]) - table.add_row(['created', image.get('createDate')]) - table.add_row(['disk_space', formatting.b_to_gb(disk_space)]) - table.add_row(['datacenters', formatting.listing(sorted(datacenters), - separator=',')]) - - return table - - -class DeleteImage(environment.CLIRunnable): - """ -usage: sl image delete [options] - -Get details for an image -""" - action = 'delete' - - def execute(self, args): - image_mgr = SoftLayer.ImageManager(self.client) - image_id = helpers.resolve_id(image_mgr.resolve_ids, - args.get(''), - 'image') - - image_mgr.delete_image(image_id) - - -class EditImage(environment.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 = SoftLayer.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 = helpers.resolve_id(image_mgr.resolve_ids, - args.get(''), 'image') - if not image_mgr.edit(image_id, **data): - raise exceptions.CLIAbort("Failed to Edit Image") diff --git a/SoftLayer/CLI/modules/iscsi.py b/SoftLayer/CLI/modules/iscsi.py deleted file mode 100644 index a0d5bbd41..000000000 --- a/SoftLayer/CLI/modules/iscsi.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -usage: sl iscsi [] [...] [options] - -Manage, order, delete iSCSI targets - -The available commands are: - 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. -""" -# from SoftLayer.CLI import (CLIRunnable, Table, no_going_back, FormattedItem) -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - - -class ListISCSIs(environment.CLIRunnable): - - """ -usage: sl iscsi list [options] - -List iSCSI targets -""" - action = 'list' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - iscsi_list = iscsi_mgr.list_iscsi() - iscsi_list = [utils.NestedDict(n) for n in iscsi_list] - table = formatting.Table([ - 'id', - 'datacenter', - 'size', - 'username', - 'password', - 'server' - ]) - for iscsi in iscsi_list: - table.add_row([ - iscsi['id'], - iscsi['serviceResource']['datacenter'].get('name', - formatting.blank()), - formatting.FormattedItem( - iscsi.get('capacityGb', formatting.blank()), - "%dGB" % iscsi.get('capacityGb', 0)), - iscsi.get('username', formatting.blank()), - iscsi.get('password', formatting.blank()), - iscsi.get('serviceResourceBackendIpAddress', - formatting.blank())]) - return table - - -class CreateISCSI(environment.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 = SoftLayer.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 exceptions.ArgumentError('Missing required options: %s' - % ','.join(invalid_args)) - - -class CancelISCSI(environment.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 = SoftLayer.ISCSIManager(self.client) - iscsi_id = helpers.resolve_id( - iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - - immediate = args.get('--immediate', False) - - reason = args.get('--reason') - if args['--really'] or formatting.no_going_back(iscsi_id): - iscsi_mgr.cancel_iscsi(iscsi_id, reason, immediate) - else: - raise exceptions.CLIAbort('Aborted') - - -class ISCSIDetails(environment.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 = SoftLayer.ISCSIManager(self.client) - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - iscsi_id = helpers.resolve_id( - iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - result = iscsi_mgr.get_iscsi(iscsi_id) - result = utils.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 = formatting.Table(['username', 'password']) - pass_table.add_row([result['username'], result['password']]) - table.add_row(['users', pass_table]) - - return table diff --git a/SoftLayer/CLI/modules/loadbal.py b/SoftLayer/CLI/modules/loadbal.py deleted file mode 100755 index cd1e46c00..000000000 --- a/SoftLayer/CLI/modules/loadbal.py +++ /dev/null @@ -1,591 +0,0 @@ -""" -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. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - - -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 exceptions.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 = formatting.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 = formatting.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 = formatting.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 = formatting.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(environment.CLIRunnable): - """ -usage: sl loadbal list [options] - -List active load balancers - -""" - action = 'list' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - load_balancers = mgr.get_local_lbs() - return get_local_lbs_table(load_balancers) - - -class LoadBalancerHealthChecks(environment.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 = SoftLayer.LoadBalancerManager(self.client) - - hc_types = mgr.get_hc_types() - table = formatting.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(environment.CLIRunnable): - """ -usage: sl loadbal routing-methods [options] - -List load balancers routing methods that can be used -""" - action = 'routing-methods' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - routing_methods = mgr.get_routing_methods() - table = formatting.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(environment.CLIRunnable): - """ -usage: sl loadbal routing-types [options] - -List load balancers routing types that can be used -""" - action = 'routing-types' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - routing_types = mgr.get_routing_types() - table = formatting.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(environment.CLIRunnable): - """ -usage: sl loadbal detail [options] - -Get Load balancer details - -""" - action = 'detail' - - def execute(self, args): - mgr = SoftLayer.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(environment.CLIRunnable): - """ -usage: sl loadbal cancel [options] - -Cancels an existing load_balancer - -""" - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - loadbal_id = int(key_value[1]) - - if args['--really'] or formatting.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 exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceDelete(environment.CLIRunnable): - """ -usage: sl loadbal service-delete [options] - -Deletes an existing load_balancer service - -""" - action = 'service-delete' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - service_id = int(key_value[1]) - - if args['--really'] or formatting.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 exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceToggle(environment.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 = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - service_id = int(key_value[1]) - - if args['--really'] or formatting.confirm("This action will toggle " - "the status on the service. " - "Continue?"): - mgr.toggle_service_status(service_id) - return 'Load balancer service %s status updated!' % input_id - else: - raise exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceEdit(environment.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 = SoftLayer.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(environment.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 = SoftLayer.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(environment.CLIRunnable): - """ -usage: sl loadbal group-delete [options] - -Deletes an existing load_balancer service group - -""" - action = 'group-delete' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - input_id = args.get('') - - key_value = get_ids(input_id) - group_id = int(key_value[1]) - - if args['--really'] or formatting.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 exceptions.CLIAbort('Aborted.') - - -class LoadBalancerServiceGroupEdit(environment.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 = SoftLayer.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(environment.CLIRunnable): - """ -usage: sl loadbal group-reset [options] - -Resets the connections on a certain service group - -""" - action = 'group-reset' - - def execute(self, args): - mgr = SoftLayer.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(environment.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 = SoftLayer.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(environment.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 = SoftLayer.LoadBalancerManager(self.client) - input_id = helpers.resolve_id( - mgr.resolve_ids, args.get(''), 'load_balancer') - if not formatting.confirm("This action will incur charges on your " - "account. Continue?"): - raise exceptions.CLIAbort('Aborted.') - mgr.add_local_lb(input_id, datacenter=args['--datacenter']) - return "Load balancer is being created!" - - -class CreateOptionsLoadBalancer(environment.CLIRunnable): - """ -usage: sl loadbal create-options - -Output available options when adding a new load balancer - -""" - action = 'create-options' - - def execute(self, args): - mgr = SoftLayer.LoadBalancerManager(self.client) - - table = formatting.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 deleted file mode 100644 index 0c39a8ad0..000000000 --- a/SoftLayer/CLI/modules/messaging.py +++ /dev/null @@ -1,522 +0,0 @@ -""" -usage: sl messaging [] [...] [options] - -Manage the SoftLayer Message Queue service. For most commands, a queue account -is required. Use 'sl messaging accounts-list' to list current accounts - -The available commands are: - accounts-list List all queue accounts - endpoints-list List all service endpoints - ping Ping the service - - queue-add Create a new queue - queue-detail Prints the details of a queue - queue-edit Modifies an existing queue - queue-list Lists out all queues on an account - queue-pop Pop a message from a queue - queue-push Pushes a message into a queue - queue-remove Delete a queue - - topic-add Creates a new topic - topic-detail Prints the details of a topic - topic-list Lists out all topics on an account - topic-push Pushes a notification to a topic - topic-remove Deletes a topic - topic-subscribe Adds a subscription on a topic - topic-unsubscribe Remove a subscription on a topic - -""" -# :license: MIT, see LICENSE for more details. -# Missing docstrings ignored due to __doc__ = __doc__ magic -# pylint: disable=C0111 -import sys - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -COMMON_MESSAGING_ARGS = """Service Options: - --datacenter=NAME Datacenter, E.G.: dal05 - --network=TYPE Network type, [Options: public, private] -""" - - -class ListAccounts(environment.CLIRunnable): - """ -usage: sl messaging accounts-list [options] - -List SoftLayer Message Queue Accounts - -""" - action = 'accounts-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - accounts = manager.list_accounts() - - table = formatting.Table([ - 'id', 'name', 'status' - ]) - for account in accounts: - if not account['nodes']: - continue - - table.add_row([ - account['nodes'][0]['accountName'], - account['name'], - account['status']['name'], - ]) - - return table - - -class ListEndpoints(environment.CLIRunnable): - """ -usage: sl messaging endpoints-list [options] - -List SoftLayer Message Queue Endpoints - -""" - action = 'endpoints-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - regions = manager.get_endpoints() - - table = formatting.Table([ - 'name', 'public', 'private' - ]) - for region, endpoints in regions.items(): - table.add_row([ - region, - endpoints.get('public') or formatting.blank(), - endpoints.get('private') or formatting.blank(), - ]) - - return table - - -class Ping(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging ping [options] - -Ping the SoftLayer Message Queue service - -""" + COMMON_MESSAGING_ARGS - action = 'ping' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - okay = manager.ping( - datacenter=args['--datacenter'], network=args['--network']) - if okay: - return 'OK' - else: - exceptions.CLIAbort('Ping failed') - - -def queue_table(queue): - """ Returns a table with details about a queue """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - 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', formatting.listing(queue['tags'] or [])]) - table.add_row(['expiration', queue['expiration']]) - table.add_row(['visibility_interval', queue['visibility_interval']]) - return table - - -def message_table(message): - """ Returns a table with details about a message """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - 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): - """ Returns a table with details about a topic """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['name', topic['name']]) - table.add_row(['tags', formatting.listing(topic['tags'] or [])]) - return table - - -def subscription_table(sub): - """ Returns a table with details about a subscription """ - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - 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(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-list [options] - -List all queues on an account - -""" + COMMON_MESSAGING_ARGS - action = 'queue-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - queues = mq_client.get_queues()['items'] - - table = formatting.Table([ - 'name', 'message_count', 'visible_message_count' - ]) - for queue in queues: - table.add_row([ - queue['name'], - queue['message_count'], - queue['visible_message_count'], - ]) - return table - - -class QueueDetail(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-detail [options] - -Detail a queue - -""" + COMMON_MESSAGING_ARGS - action = 'queue-detail' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - queue = mq_client.get_queue(args['']) - return queue_table(queue) - - -class QueueCreate(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-add [options] - -Create a queue - -Options: - --visibility_interval=SECONDS Time in seconds that messages will re-appear - after being popped - --expiration=SECONDS Time in seconds that messages will live - --tags=TAGS Comma-separated list of tags - -""" + COMMON_MESSAGING_ARGS - action = 'queue-add' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - queue = mq_client.create_queue( - args[''], - visibility_interval=int(args.get('--visibility_interval') or 30), - expiration=int(args.get('--expiration') or 604800), - tags=tags, - ) - return queue_table(queue) - - -class QueueModify(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-edit [options] - -Modify a queue - -Options: - --visibility_interval=SECONDS Time in seconds that messages will re-appear - after being popped - --expiration=SECONDS Time in seconds that messages will live - --tags=TAGS Comma-separated list of tags - -""" + COMMON_MESSAGING_ARGS - action = 'queue-edit' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - queue = mq_client.create_queue( - args[''], - visibility_interval=int(args.get('--visibility_interval') or 30), - expiration=int(args.get('--expiration') or 604800), - tags=tags, - ) - return queue_table(queue) - - -class QueueDelete(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-remove [] - [options] - -Delete a queue or a queued message - -Options: - --force Flag to force the deletion of the queue even when there are messages - -""" + COMMON_MESSAGING_ARGS - action = 'queue-remove' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - if args['']: - mq_client.delete_message(args[''], - args['']) - else: - mq_client.delete_queue(args[''], args.get('--force')) - - -class QueuePush(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-push ( | -) - [options] - -Push a message into a queue - -Options: - --force Flag to force the deletion of the queue even when there are messages - -""" + COMMON_MESSAGING_ARGS - action = 'queue-push' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - body = '' - if args[''] == '-': - body = sys.stdin.read() - else: - body = args[''] - return message_table( - mq_client.push_queue_message(args[''], body)) - - -class QueuePop(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging queue-pop [options] - -Pops a message from a queue - -Options: - --count=NUM Count of messages to pop - --delete-after Remove popped messages from the queue - -""" + COMMON_MESSAGING_ARGS - action = 'queue-pop' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - messages = mq_client.pop_messages( - args[''], - args.get('--count') or 1) - formatted_messages = [] - for message in messages['items']: - formatted_messages.append(message_table(message)) - - if args.get('--delete-after'): - for message in messages['items']: - mq_client.delete_message( - args[''], - message['id']) - return formatted_messages - - -class TopicList(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-list [options] - -List all topics on an account - -""" + COMMON_MESSAGING_ARGS - action = 'topic-list' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - topics = mq_client.get_topics()['items'] - - table = formatting.Table(['name']) - for topic in topics: - table.add_row([topic['name']]) - return table - - -class TopicDetail(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-detail [options] - -Detail a topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-detail' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - topic = mq_client.get_topic(args['']) - subscriptions = mq_client.get_subscriptions(args['']) - tables = [] - for sub in subscriptions['items']: - tables.append(subscription_table(sub)) - return [topic_table(topic), tables] - - -class TopicCreate(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-add [options] - -Create a new topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-add' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - topic = mq_client.create_topic( - args[''], - visibility_interval=int( - args.get('--visibility_interval') or 30), - expiration=int(args.get('--expiration') or 604800), - tags=tags, - ) - return topic_table(topic) - - -class TopicDelete(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-remove [options] - -Delete a topic or subscription - -Options: - --force Flag to force the deletion of the topic even when there are - subscriptions -""" + COMMON_MESSAGING_ARGS - action = 'topic-remove' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - mq_client.delete_topic(args[''], args.get('--force')) - - -class TopicSubscribe(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-subscribe [options] - -Create a subscription on a topic - -Options: - --type=TYPE Type of endpoint, [Options: http, queue] - --queue-name=NAME Queue name. Required if --type is queue - --http-method=METHOD HTTP Method to use if --type is http - --http-url=URL HTTP/HTTPS URL to use. Required if --type is http - --http-body=BODY HTTP Body template to use if --type is http - -""" + COMMON_MESSAGING_ARGS - action = 'topic-subscribe' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - if args['--type'] == 'queue': - subscription = mq_client.create_subscription( - args[''], - 'queue', - queue_name=args['--queue-name'], - ) - elif args['--type'] == 'http': - subscription = mq_client.create_subscription( - args[''], - 'http', - method=args['--http-method'] or 'GET', - url=args['--http-url'], - body=args['--http-body'] - ) - else: - raise exceptions.ArgumentError( - '--type should be either queue or http.') - return subscription_table(subscription) - - -class TopicUnsubscribe(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-unsubscribe - [options] - -Remove a subscription on a topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-unsubscribe' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - mq_client.delete_subscription( - args[''], - args['']) - - -class TopicPush(environment.CLIRunnable): - __doc__ = """ -usage: sl messaging topic-push ( | -) - [options] - -Push a message into a topic - -""" + COMMON_MESSAGING_ARGS - action = 'topic-push' - - def execute(self, args): - manager = SoftLayer.MessagingManager(self.client) - mq_client = manager.get_connection(args['']) - - # the message body comes from the positional argument or stdin - body = '' - 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 deleted file mode 100644 index 4bf89d9b4..000000000 --- a/SoftLayer/CLI/modules/metadata.py +++ /dev/null @@ -1,243 +0,0 @@ -""" -usage: sl metadata [] [...] [options] - -Find details about this machine. These commands only work on devices on the -backend SoftLayer network. This allows for self-discovery for newly provisioned -resources. - -The available commands are: - backend_ip Primary backend ip address - backend_mac Backend mac addresses - datacenter Datacenter name - datacenter_id Datacenter id - fqdn Fully qualified domain name - frontend_mac Frontend mac addresses - hostname Hostname - id Id - ip Primary ip address - network Details about either the public or private network - provision_state Provision state - tags Tags - user_data User-defined data -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -class MetaRunnable(environment.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) - except SoftLayer.TransportError: - raise exceptions.CLIAbort( - 'Cannot connect to the backend service address. Make sure ' - 'this command is being ran from a device on the backend ' - 'network.') - - def _execute(self, _): - """ To be overridden exactly like the execute() method """ - pass - - -class BackendMacAddresses(MetaRunnable): - """ -usage: sl metadata backend_mac [options] - -List backend mac addresses -""" - action = 'backend_mac' - - def _execute(self, _): - backend_macs = SoftLayer.MetadataManager().get('backend_mac') - return formatting.listing(backend_macs, separator=',') - - -class Datacenter(MetaRunnable): - """ -usage: sl metadata datacenter [options] - -Get datacenter name -""" - action = 'datacenter' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('datacenter') - - -class DatacenterId(MetaRunnable): - """ -usage: sl metadata datacenter_id [options] - -Get datacenter id -""" - action = 'datacenter_id' - - def _execute(self, _): - return str(SoftLayer.MetadataManager().get('datacenter_id')) - - -class FrontendMacAddresses(MetaRunnable): - """ -usage: sl metadata frontend_mac [options] - -List frontend mac addresses -""" - action = 'frontend_mac' - - def _execute(self, _): - frontend_macs = SoftLayer.MetadataManager().get('frontend_mac') - return formatting.listing(frontend_macs, separator=',') - - -class FullyQualifiedDomainName(MetaRunnable): - """ -usage: sl metadata fqdn [options] - -Get fully qualified domain name -""" - action = 'fqdn' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('fqdn') - - -class Hostname(MetaRunnable): - """ -usage: sl metadata hostname [options] - -Get hostname -""" - action = 'hostname' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('hostname') - - -class Id(MetaRunnable): - """ -usage: sl metadata id - -Get id -""" - action = 'id' - - def _execute(self, _): - return str(SoftLayer.MetadataManager().get('id')) - - -class PrimaryBackendIpAddress(MetaRunnable): - """ -usage: sl metadata backend_ip [options] - -Get primary backend ip address -""" - action = 'backend_ip' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('primary_backend_ip') - - -class PrimaryIpAddress(MetaRunnable): - """ -usage: sl metadata ip [options] - -Get primary ip address -""" - action = 'ip' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('primary_ip') - - -class ProvisionState(MetaRunnable): - """ -usage: sl metadata provision_state [options] - -Get provision state -""" - action = 'provision_state' - - def _execute(self, _): - return SoftLayer.MetadataManager().get('provision_state') - - -class Tags(MetaRunnable): - """ -usage: sl metadata tags [options] - -List tags -""" - action = 'tags' - - def _execute(self, _): - return formatting.listing(SoftLayer.MetadataManager().get('tags'), - separator=',') - - -class UserMetadata(MetaRunnable): - """ -usage: sl metadata user_data [options] - -Get user-defined data -""" - action = 'user_data' - - def _execute(self, _): - """ Returns user metadata """ - userdata = SoftLayer.MetadataManager().get('user_data') - if userdata: - return userdata - else: - raise exceptions.CLIAbort("No user metadata.") - - -class Network(MetaRunnable): - """ -usage: sl metadata network ( | ) [options] - -Get details about the public or private network -""" - action = 'network' - - def _execute(self, args): - meta = SoftLayer.MetadataManager() - if args['']: - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - network = meta.public_network() - table.add_row([ - 'mac addresses', - formatting.listing(network['mac_addresses'], separator=',')]) - table.add_row([ - 'router', network['router']]) - table.add_row([ - 'vlans', formatting.listing(network['vlans'], separator=',')]) - table.add_row([ - 'vlan ids', - formatting.listing(network['vlan_ids'], separator=',')]) - return table - - if args['']: - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - network = meta.private_network() - table.add_row([ - 'mac addresses', - formatting.listing(network['mac_addresses'], separator=',')]) - table.add_row([ - 'router', network['router']]) - table.add_row([ - 'vlans', formatting.listing(network['vlans'], separator=',')]) - table.add_row([ - 'vlan ids', - formatting.listing(network['vlan_ids'], separator=',')]) - return table diff --git a/SoftLayer/CLI/modules/nas.py b/SoftLayer/CLI/modules/nas.py deleted file mode 100644 index 860e7bf09..000000000 --- a/SoftLayer/CLI/modules/nas.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -usage: sl nas [] [...] [options] - -Manage NAS accounts - -The available commands are: - list List NAS accounts -""" -# :license: MIT, see LICENSE for more details. - -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer import utils - - -class ListNAS(environment.CLIRunnable): - """ -usage: sl nas list [options] - -List NAS accounts - -Options: -""" - action = 'list' - - def execute(self, args): - account = self.client['Account'] - - nas_accounts = account.getNasNetworkStorage( - mask='eventCount,serviceResource[datacenter.name]') - - table = formatting.Table(['id', 'datacenter', 'size', 'username', - 'password', 'server']) - - for nas_account in nas_accounts: - table.add_row([ - nas_account['id'], - utils.lookup(nas_account, - 'serviceResource', - 'datacenter', - 'name') or formatting.blank(), - formatting.FormattedItem( - nas_account.get('capacityGb', formatting.blank()), - "%dGB" % nas_account.get('capacityGb', 0)), - nas_account.get('username', formatting.blank()), - nas_account.get('password', formatting.blank()), - nas_account.get('serviceResourceBackendIpAddress', - formatting.blank())]) - - return table diff --git a/SoftLayer/CLI/modules/rwhois.py b/SoftLayer/CLI/modules/rwhois.py deleted file mode 100644 index c036488a0..000000000 --- a/SoftLayer/CLI/modules/rwhois.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -usage: sl rwhois [] [...] [options] - -Manage the RWhoIs information on the account. - -The available commands are: - edit Edit the RWhois data on the account - show Show the RWhois data on the account -""" -# :license: MIT, see LICENSE for more details. - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -class RWhoisEdit(environment.CLIRunnable): - """ -usage: sl rwhois edit [options] - -Updates the RWhois information on your account. Only the fields you -specify will be changed. To clear a value, specify an empty string like: "" - -Options: - --abuse=EMAIL Set the abuse email - --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 - --lastname=NAME Update the last name field - --postal=CODE Set the postal code field - --private Flags the address as a private residence. - --public Flags the address as a public residence. - --state=STATE Set the state information. Use the two-letter - abbreviation. -""" - action = 'edit' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - - update = { - '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'), - 'last_name': args.get('--lastname'), - 'postal_code': args.get('--postal'), - 'state': args.get('--state') - } - - if args.get('--private'): - update['private_residence'] = False - elif args.get('--public'): - update['private_residence'] = True - - check = [x for x in update.values() if x is not None] - if not check: - raise exceptions.CLIAbort( - "You must specify at least one field to update.") - - mgr.edit_rwhois(**update) # pylint: disable=W0142 - - -class RWhoisShow(environment.CLIRunnable): - """ -usage: sl rwhois show [options] - -Display the RWhois information for your account. -""" - action = 'show' - - def execute(self, args): - mgr = SoftLayer.NetworkManager(self.client) - result = mgr.get_rwhois() - - table = formatting.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'): - 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 table diff --git a/SoftLayer/CLI/modules/server.py b/SoftLayer/CLI/modules/server.py deleted file mode 100644 index e49891991..000000000 --- a/SoftLayer/CLI/modules/server.py +++ /dev/null @@ -1,1107 +0,0 @@ -""" -usage: sl server [] [...] [options] - sl server [-h | --help] - -Manage hardware servers - -The available commands are: - cancel Cancel a dedicated server. - cancel-reasons Provides the list of possible cancellation reasons - create Create a new dedicated server - create-options Display a list of creation options for a specific chassis - detail Retrieve hardware details - list List hardware devices - list-chassis Provide a list of all chassis available for ordering - nic-edit Edit NIC settings - power-cycle Issues power cycle to server - power-off Powers off a running server - power-on Boots up a server - reboot Reboots a running server - reload Perform an OS reload - -For several commands, will be asked for. This can be the id, -hostname or the ip address for a piece of hardware. -""" -# :license: MIT, see LICENSE for more details. -import os -import re - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer.CLI import template -from SoftLayer import utils - - -class ListServers(environment.CLIRunnable): - """ -usage: sl server list [options] - -List hardware servers on the account - -Examples: - sl server list --datacenter=dal05 - sl server list --network=100 --domain=example.com - sl server list --tags=production,db - -Options: - --sortby=ARG Column to sort by. options: id, datacenter, host, cores, - memory, primary_ip, backend_ip - -Filters: - -c, --cpu=CPU Number of CPU cores - -D, --domain=DOMAIN Domain portion of the FQDN. example: example.com - -d, --datacenter=DC Datacenter shortname (sng01, dal05, ...) - -H, --hostname=HOST Host portion of the FQDN. example: server - -m, --memory=MEMORY Memory in gigabytes - -n, --network=MBPS Network port speed in Mbps - --tags=ARG Only show instances that have one of these tags. - Comma-separated. (production,db) - -For more on filters see 'sl help filters' -""" - action = 'list' - - def execute(self, args): - manager = SoftLayer.HardwareManager(self.client) - - tags = None - if args.get('--tags'): - tags = [tag.strip() for tag in args.get('--tags').split(',')] - - servers = manager.list_hardware( - hostname=args.get('--hostname'), - domain=args.get('--domain'), - cpus=args.get('--cpu'), - memory=args.get('--memory'), - datacenter=args.get('--datacenter'), - nic_speed=args.get('--network'), - tags=tags) - - table = formatting.Table([ - 'id', - 'datacenter', - 'host', - 'cores', - 'memory', - 'primary_ip', - 'backend_ip', - 'active_transaction', - 'owner' - ]) - table.sortby = args.get('--sortby') or 'host' - - for server in servers: - server = utils.NestedDict(server) - - table.add_row([ - server['id'], - server['datacenter']['name'] or formatting.blank(), - server['fullyQualifiedDomainName'], - server['processorPhysicalCoreAmount'], - formatting.gb(server['memoryCapacity'] or 0), - server['primaryIpAddress'] or formatting.blank(), - server['primaryBackendIpAddress'] or formatting.blank(), - formatting.active_txn(server), - utils.lookup( - server, 'billingItem', 'orderItem', 'order', 'userRecord', - 'username') or formatting.blank(), - ]) - - return table - - -class ServerDetails(environment.CLIRunnable): - """ -usage: sl server detail [--passwords] [--price] [options] - -Get details for a hardware device - -Options: - --passwords Show passwords (check over your shoulder!) - --price Show associated prices -""" - action = 'detail' - - def execute(self, args): - hardware = SoftLayer.HardwareManager(self.client) - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['Name'] = 'r' - table.align['Value'] = 'l' - - hardware_id = helpers.resolve_id( - hardware.resolve_ids, args.get(''), 'hardware') - result = hardware.get_hardware(hardware_id) - result = utils.NestedDict(result) - - 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 formatting.blank()]) - table.add_row(['cores', result['processorPhysicalCoreAmount']]) - table.add_row(['memory', - formatting.gb(result['memoryCapacity'])]) - table.add_row(['public_ip', - result['primaryIpAddress'] or formatting.blank()]) - table.add_row(['private_ip', - result['primaryBackendIpAddress'] - or formatting.blank()]) - table.add_row(['ipmi_ip', - result['networkManagementIpAddress'] - or formatting.blank()]) - table.add_row([ - 'os', - formatting.FormattedItem( - result['operatingSystem']['softwareLicense'] - ['softwareDescription']['referenceCode'] or formatting.blank(), - result['operatingSystem']['softwareLicense'] - ['softwareDescription']['name'] or formatting.blank() - )]) - - table.add_row( - ['created', result['provisionDate'] or formatting.blank()]) - - table.add_row(['owner', formatting.FormattedItem( - utils.lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', - 'username') or formatting.blank() - )]) - - vlan_table = formatting.Table(['type', 'number', 'id']) - - for vlan in result['networkVlans']: - vlan_table.add_row([ - vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) - table.add_row(['vlans', vlan_table]) - - if result.get('notes'): - table.add_row(['notes', result['notes']]) - - if args.get('--price'): - 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'])) - table.add_row(['users', formatting.listing(user_strs)]) - - tag_row = [] - for tag in result['tagReferences']: - tag_row.append(tag['tag']['name']) - - if tag_row: - table.add_row(['tags', formatting.listing(tag_row, separator=',')]) - - # Test to see if this actually has a primary (public) ip address - try: - if not result['privateNetworkOnlyFlag']: - ptr_domains = (self.client['Hardware_Server'] - .getReverseDomainRecords(id=hardware_id)) - - for ptr_domain in ptr_domains: - for ptr in ptr_domain['resourceRecords']: - table.add_row(['ptr', ptr['data']]) - except SoftLayer.SoftLayerAPIError: - pass - return table - - -class ServerReload(environment.CLIRunnable): - """ -usage: sl server reload [--key=KEY...] [options] - -Reload the OS on a hardware server based on its current configuration - -Optional: - -i, --postinstall=URI Post-install script to download - (Only HTTPS executes, HTTP leaves file in /root) - -k, --key=KEY SSH keys to add to the root user. Can be specified - multiple times -""" - - action = 'reload' - options = ['confirm'] - - def execute(self, args): - hardware = SoftLayer.HardwareManager(self.client) - hardware_id = helpers.resolve_id( - hardware.resolve_ids, args.get(''), 'hardware') - keys = [] - if args.get('--key'): - for key in args.get('--key'): - resolver = SoftLayer.SshKeyManager(self.client).resolve_ids - key_id = helpers.resolve_id(resolver, key, 'SshKey') - keys.append(key_id) - if args['--really'] or formatting.no_going_back(hardware_id): - hardware.reload(hardware_id, args['--postinstall'], keys) - else: - raise exceptions.CLIAbort('Aborted') - - -class CancelServer(environment.CLIRunnable): - """ -usage: sl server cancel [options] - -Cancel a dedicated server - -Options: - --immediate Cancels the server immediately (instead of on the billing - anniversary) - --comment=COMMENT An optional comment to add to the cancellation ticket - --reason=REASON An optional cancellation reason. See cancel-reasons for a - list of available options -""" - - action = 'cancel' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id( - mgr.resolve_ids, args.get(''), 'hardware') - - comment = args.get('--comment') - - if not comment and not args['--really']: - comment = self.env.input("(Optional) Add a cancellation comment:") - - reason = args.get('--reason') - immediate = args.get('--immediate') - - if args['--really'] or formatting.no_going_back(hw_id): - mgr.cancel_hardware(hw_id, reason, comment, immediate) - else: - raise exceptions.CLIAbort('Aborted') - - -class ServerCancelReasons(environment.CLIRunnable): - """ -usage: sl server cancel-reasons - -Display a list of cancellation reasons -""" - - action = 'cancel-reasons' - - def execute(self, args): - table = formatting.Table(['Code', 'Reason']) - table.align['Code'] = 'r' - table.align['Reason'] = 'l' - - mgr = SoftLayer.HardwareManager(self.client) - - for code, reason in mgr.get_cancellation_reasons().items(): - table.add_row([code, reason]) - - return table - - -class ServerPowerOff(environment.CLIRunnable): - """ -usage: sl server power-off [options] - -Power off an active server -""" - action = 'power-off' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - if args['--really'] or formatting.confirm('This will power off the ' - 'server with id %s ' - 'Continue?' % hw_id): - self.client['Hardware_Server'].powerOff(id=hw_id) - else: - raise exceptions.CLIAbort('Aborted.') - - -class ServerReboot(environment.CLIRunnable): - """ -usage: sl server reboot [--hard | --soft] [options] - -Reboot an active server - -Optional: - --hard Perform an abrupt reboot - --soft Perform a graceful reboot -""" - action = 'reboot' - options = ['confirm'] - - def execute(self, args): - hardware_server = self.client['Hardware_Server'] - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - if args['--really'] or formatting.confirm('This will power off the ' - 'server with id %s. ' - 'Continue?' % hw_id): - if args['--hard']: - hardware_server.rebootHard(id=hw_id) - elif args['--soft']: - hardware_server.rebootSoft(id=hw_id) - else: - hardware_server.rebootDefault(id=hw_id) - else: - raise exceptions.CLIAbort('Aborted.') - - -class ServerPowerOn(environment.CLIRunnable): - """ -usage: sl server power-on [options] - -Power on a server -""" - action = 'power-on' - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - self.client['Hardware_Server'].powerOn(id=hw_id) - - -class ServerPowerCycle(environment.CLIRunnable): - """ -usage: sl server power-cycle [options] - -Issues power cycle to server via the power strip -""" - action = 'power-cycle' - options = ['confirm'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - - if args['--really'] or formatting.confirm('This will power off the ' - 'server with id %s. ' - 'Continue?' % hw_id): - self.client['Hardware_Server'].powerCycle(id=hw_id) - else: - raise exceptions.CLIAbort('Aborted.') - - -class NicEditServer(environment.CLIRunnable): - """ -usage: sl server nic-edit (public | private) --speed=SPEED - [options] - -Manage NIC settings - -Options: - --speed=SPEED Port speed. 0 disables the port. - [Options: 0, 10, 100, 1000, 10000] -""" - action = 'nic-edit' - - def execute(self, args): - public = args['public'] - - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - - mgr.change_port_speed(hw_id, public, args['--speed']) - - -class ListChassisServer(environment.CLIRunnable): - """ -usage: sl server list-chassis [options] - -Display a list of chassis available for ordering dedicated servers. -""" - action = 'list-chassis' - - def execute(self, args): - table = formatting.Table(['Code', 'Chassis']) - table.align['Code'] = 'r' - table.align['Chassis'] = 'l' - - mgr = SoftLayer.HardwareManager(self.client) - chassis = mgr.get_available_dedicated_server_packages() - - for chassis in chassis: - table.add_row([chassis[0], chassis[1]]) - - return table - - -class ServerRescue(environment.CLIRunnable): - """ -usage: sl server rescue [options] - -Reboot server into a rescue image - - -""" - action = 'rescue' - options = ['confirm'] - - def execute(self, args): - server = SoftLayer.HardwareManager(self.client) - server_id = helpers.resolve_id(server.resolve_ids, - args.get(''), - 'hardware') - - if args['--really'] or formatting.confirm( - "This action will reboot this server. " - "Continue?"): - - server.rescue(server_id) - else: - raise exceptions.CLIAbort('Aborted') - - -class ServerCreateOptions(environment.CLIRunnable): - """ -usage: sl server create-options [options] - -Output available available options when creating a dedicated server with the -specified chassis. - -Options: - --all Show all options. default if no other option provided - --controller Show disk controller options - --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', - 'controller'] - - def execute(self, args): - mgr = SoftLayer.HardwareManager(self.client) - - table = formatting.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 exceptions.CLIAbort('Invalid chassis specified.') - - ds_options = mgr.get_dedicated_server_create_options(chassis_id) - - show_all = True - for opt_name in self.options: - if args.get("--" + opt_name): - show_all = False - break - - 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] - - table.add_row([results[0], formatting.listing(sorted(results[1]))]) - - if (args['--cpu'] or show_all) and not bmc: - results = self.get_create_options(ds_options, 'cpu') - - cpu_table = formatting.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]]) - table.add_row(['cpu', cpu_table]) - - if (args['--memory'] or show_all) and not bmc: - results = self.get_create_options(ds_options, 'memory')[0] - - table.add_row([results[0], formatting.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 = formatting.Table(['memory', 'cpu']) - for result in results: - memory_cpu_table.add_row([ - result[0], - formatting.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: - table.add_row([ - result[0], - formatting.listing( - [item[0] for item in sorted(result[1])], - separator=os.linesep - )]) - - if args['--disk'] or show_all: - results = self.get_create_options(ds_options, 'disk')[0] - - table.add_row([ - results[0], - formatting.listing( - [item[0] for item in sorted(results[1])], - separator=os.linesep - )]) - - if args['--nic'] or show_all: - results = self.get_create_options(ds_options, 'nic') - - for result in results: - table.add_row([result[0], formatting.listing( - item[0] for item in sorted(result[1],))]) - - if (args['--controller'] or show_all) and not bmc: - results = self.get_create_options(ds_options, 'disk_controller')[0] - - table.add_row([results[0], formatting.listing( - item[0] for item in sorted(results[1],))]) - - return table - - def get_create_options(self, ds_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 ds_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. - """ - return_value = None - - if 'datacenter' == section: - datacenters = [loc['keyname'] - for loc in ds_options['locations']] - return_value = [('datacenter', datacenters)] - elif 'cpu' == section and 'server' in ds_options['categories']: - results = [] - - for item in ds_options['categories']['server']['items']: - results.append(( - item['description'], - item['price_id'] - )) - - 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_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) - - 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) - - 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_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' - (.+)\(') - - os_list = {} - flat_list = [] - - # 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(opsys['description']) - - # Skip this operating system if it's not parsable - if os_results is None: - continue - - name = os_results.group(1) - version = os_results.group(2) - 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 = 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, opsys['price_id'])) - flat_list.append((os_code, opsys['price_id'])) - - if pretty: - results = [] - for opsys in sorted(os_list.keys()): - results.append(('os (%s)' % opsys, os_list[opsys])) - - return_value = results - else: - return_value = [('os', flat_list)] - - elif 'disk' == section: - disks = [] - 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) - - disk_type = disk_type.replace('RPM', '').strip() - disk_type = disk_type.replace(' ', '_').upper() - disk_type = str(int(disk['capacity'])) + '_' + disk_type - disks.append((disk_type, disk['price_id'], disk['id'])) - - return_value = [('disk', disks)] - elif 'nic' == section: - single = [] - dual = [] - - for item in ds_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_value = [('single nic', single), ('dual nic', dual)] - elif 'disk_controller' == section: - options = [] - for item in ds_options['categories']['disk_controller']['items']: - text = item['description'].replace(' ', '') - - if 'Non-RAID' == text: - text = 'None' - - options.append((text, item['price_id'])) - - 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(environment.CLIRunnable): - """ -usage: sl server create [--disk=SIZE...] [--key=KEY...] [options] - -Order/create a dedicated server. See 'sl server list-chassis' and -'sl server create-options' for valid options. - -Required: - -H --hostname=HOST Host portion of the FQDN. example: server - -D --domain=DOMAIN Domain portion of the FQDN. example: example.com - --chassis=CHASSIS The chassis to use for the new server - -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 - --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. - --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'] - required_params = ['--hostname', '--domain', '--chassis', '--cpu', - '--memory', '--os'] - - def execute(self, args): - template.update_with_template_args(args, list_args=['--disk', '--key']) - mgr = SoftLayer.HardwareManager(self.client) - self._validate_args(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 = formatting.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(formatting.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') - template.export_to_template(export_file, args, - exclude=['--wait', '--test']) - return 'Successfully exported options to a template file.' - - if do_create: - if args['--really'] or formatting.confirm( - "This action will incur charges on your account. " - "Continue?"): - result = mgr.place_order(**order) - - table = formatting.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 exceptions.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 = SoftLayer.HardwareManager(self.client) - - order = { - 'hostname': args['--hostname'], - 'domain': args['--domain'], - 'bare_metal': False, - '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']) - - if os_price: - order['os'] = os_price - else: - raise exceptions.CLIAbort('Invalid operating system specified.') - - order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' - - 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 - for disk in args.get('--disk'): - disk_price = self._get_disk_price(ds_options, disk, disk_number) - disk_number += 1 - if disk_price: - disk_prices.append(disk_price) - - if not disk_prices: - disk_prices.append(self._get_default_value(ds_options, 'disk0')) - - order['disks'] = disk_prices - - # Set the disk controller price - 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 - - # Set the port speed - port_speed = args.get('--network') or '100' - - nic_price = self._get_price_id_from_options(ds_options, 'nic', - port_speed) - - if nic_price: - order['port_speed'] = nic_price - else: - raise exceptions.CLIAbort('Invalid NIC speed specified.') - - if args.get('--postinstall'): - order['post_uri'] = args.get('--postinstall') - - # Get the SSH keys - if args.get('--key'): - keys = [] - for key in args.get('--key'): - resolver = SoftLayer.SshKeyManager(self.client).resolve_ids - key_id = helpers.resolve_id(resolver, 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'] - - 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 exceptions.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 - - for item in ds_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_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 - # we can then use to get the price ID for our specific disk - item_id = self._get_price_id_from_options(ds_options, 'disk', - value, True) - key = 'disk' + str(number) - if key in ds_options['categories']: - for item in ds_options['categories'][key]['items']: - 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 _, 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] - return item_options[2] - - -class EditServer(environment.CLIRunnable): - """ -usage: sl server edit [options] - -Edit hardware details - -Options: - -D --domain=DOMAIN Domain portion of the FQDN example: example.com - -F --userfile=FILE Read userdata from file - -H --hostname=HOST Host portion of the FQDN. example: server - -u --userdata=DATA User defined metadata string -""" - action = 'edit' - - def execute(self, args): - data = {} - - if args['--userdata'] and args['--userfile']: - raise exceptions.ArgumentError( - '[-u | --userdata] not allowed with [-F | --userfile]') - if args['--userfile']: - if not os.path.exists(args['--userfile']): - raise exceptions.ArgumentError( - 'File does not exist [-u | --userfile] = %s' - % args['--userfile']) - - if args.get('--userdata'): - data['userdata'] = args['--userdata'] - elif args.get('--userfile'): - with open(args['--userfile'], 'r') as userfile: - data['userdata'] = userfile.read() - - data['hostname'] = args.get('--hostname') - data['domain'] = args.get('--domain') - - mgr = SoftLayer.HardwareManager(self.client) - hw_id = helpers.resolve_id(mgr.resolve_ids, - args.get(''), - 'hardware') - if not mgr.edit(hw_id, **data): - raise exceptions.CLIAbort("Failed to update hardware") diff --git a/SoftLayer/CLI/modules/snapshot.py b/SoftLayer/CLI/modules/snapshot.py deleted file mode 100644 index a7d95b3b5..000000000 --- a/SoftLayer/CLI/modules/snapshot.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -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. -""" -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers -from SoftLayer import utils - - -class CreateSnapshot(environment.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 = SoftLayer.ISCSIManager(self.client) - iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - notes = args.get('--notes') - iscsi_mgr.create_snapshot(iscsi_id, notes) - - -class CreateSnapshotSpace(environment.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 = SoftLayer.ISCSIManager(self.client) - invalid_args = [k for k in self.required_params if args.get(k) is None] - if invalid_args: - raise exceptions.ArgumentError('Missing required options: %s' - % ','.join(invalid_args)) - iscsi_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - capacity = args.get('--capacity') - iscsi_mgr.create_snapshot_space(iscsi_id, capacity) - - -class CancelSnapshot(environment.CLIRunnable): - - """ -usage: sl snapshot cancel [options] - -Cancel/Delete iSCSI snapshot. - -""" - action = 'cancel' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - snapshot_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'Snapshot') - iscsi_mgr.delete_snapshot(snapshot_id) - - -class RestoreVolumeFromSnapshot(environment.CLIRunnable): - - """ -usage: sl snapshot restore-volume - -restores volume from existing snapshot. - -""" - action = 'restore-volume' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - volume_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'iSCSI') - snapshot_id = helpers.resolve_id(iscsi_mgr.resolve_ids, - args.get(''), - 'Snapshot') - iscsi_mgr.restore_from_snapshot(volume_id, snapshot_id) - - -class ListSnapshots(environment.CLIRunnable): - - """ -usage: sl snapshot list - -List iSCSI Snapshots -""" - action = 'list' - - def execute(self, args): - iscsi_mgr = SoftLayer.ISCSIManager(self.client) - iscsi_id = helpers.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 = [utils.NestedDict(n) for n in snapshots] - - table = formatting.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 deleted file mode 100644 index 9649f355f..000000000 --- a/SoftLayer/CLI/modules/sshkey.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -usage: sl sshkey [] [...] [options] - -Manage SSH keys - -The available commands are: - add Add a new SSH key to your account - remove Removes an SSH key - edit Edits information about the SSH key - list Display a list of SSH keys on your account - print Prints out an SSH key -""" -# :license: MIT, see LICENSE for more details. - -from os import path - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - - -class AddSshKey(environment.CLIRunnable): - """ -usage: sl sshkey add