From 17abaa0fcda2de9fe18a99a1746474da402af76e Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 19 Jul 2013 16:55:32 -0500 Subject: [PATCH 001/168] Version bump to v2.3.0 --- CHANGELOG | 29 +++++++++++++++++++--- SoftLayer/consts.py | 2 +- SoftLayer/tests/managers/metadata_tests.py | 2 +- docs/conf.py | 4 +-- setup.py | 2 +- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4a4701907..a3c15562b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,26 @@ +2.3.0 + + * Several bug fixes and improvements + + * Removed Python 2.5 support. Some stuff MIGHT work with 2.5 but it is no longer tested + + * API: Refactored managers into their own module to not clutter the top level + + * CLI+API: Added much more hardware support: Filters for hardware listing, dedicated server/bare metal cloud ordering, hardware cancellation + + * CLI+API: Added DNS Zone filtering (server side) + + * CLI+API: Added Post Install script support for CCIs and hardware + + * CLI: Added Message queue functionality + + * CLI: Added --debug option to CLI commands + + * API: Added more logging + + * API: Added token-based auth so you can use the API bindings with your username/password if you want. (It's still highly recommended to use your API key instead of your password) + + 2.2.0 * Consistency changes/bug fixes @@ -8,8 +31,8 @@ * CCI: Adds a way to block until transactions are done on a CCI - * CLI(CCI): For most commands, you can specify id, hostname, private ip or public ip as + * CLI: For most CCI commands, you can specify id, hostname, private ip or public ip as - * CLI(CCI): Adds the ability to filter list results for CCIs + * 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. + * 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/SoftLayer/consts.py b/SoftLayer/consts.py index 5550693b9..99b3e25e4 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -6,7 +6,7 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -VERSION = 'v2.2.0' +VERSION = 'v2.3.0' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3/' diff --git a/SoftLayer/tests/managers/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py index 15ce5f6bf..0004e9b1c 100644 --- a/SoftLayer/tests/managers/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -92,7 +92,7 @@ def test_basic(self, make_api_call): make_api_call.assert_called_with( 'GET', self.url, timeout=5, - http_headers={'User-Agent': 'SoftLayer Python v2.2.0'}) + http_headers={'User-Agent': 'SoftLayer Python v2.3.0'}) self.assertEqual(make_api_call(), r) @patch('SoftLayer.managers.metadata.make_rest_api_call') diff --git a/docs/conf.py b/docs/conf.py index e8ddfdeb8..01d493dd1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,9 +49,9 @@ # built documents. # # The short X.Y version. -version = '2.2.0' +version = '2.3.0' # The full version, including alpha/beta/rc tags. -release = '2.2.0' +release = '2.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 02e30e307..a8a7fc608 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ setup( name='SoftLayer', - version='2.2.0', + version='2.3.0', description=description, long_description=long_description, author='SoftLayer Technologies, Inc.', From fa3ff40a7301143dda6102529d4ce6e62ad46ac9 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 22 Jul 2013 11:36:31 -0500 Subject: [PATCH 002/168] Adds Password Auth to CLI Configuration This change introduces the ability for 'sl config setup' to be given your username/password and it will go out and grab your API key or generate one if one does not exist. * Moves format_output to SoftLayer.CLI.helpers * API Key/Password input is masked Example Usage: $ sl config setup Username: someuser API Key or Password: Endpoint URL [https://api.softlayer.com/xmlrpc/v3/]: :..............:..................................................................: : Name : Value : :..............:..................................................................: : Username : someuser : : API Key : oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha : : Endpoint URL : https://api.softlayer.com/xmlrpc/v3/ : :..............:..................................................................: Are you sure you want to write settings to "/Users/username/.softlayer"? [Y/n]: y --- SoftLayer/CLI/core.py | 75 +++++------------------ SoftLayer/CLI/environment.py | 4 ++ SoftLayer/CLI/helpers.py | 56 ++++++++++++++++- SoftLayer/CLI/modules/config.py | 77 +++++++++++++++++------- SoftLayer/tests/CLI/core_tests.py | 71 ++++------------------ SoftLayer/tests/CLI/environment_tests.py | 6 ++ SoftLayer/tests/CLI/helper_tests.py | 57 ++++++++++++++++++ 7 files changed, 203 insertions(+), 143 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index e795f6975..eecb5bd88 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -29,17 +29,13 @@ # :license: BSD, see LICENSE for more details. import sys -import os -import os.path import logging -from prettytable import FRAME, NONE from docopt import docopt, DocoptExit -from SoftLayer import Client, SoftLayerError +from SoftLayer import Client, SoftLayerError, SoftLayerAPIError from SoftLayer.consts import VERSION -from SoftLayer.CLI.helpers import ( - Table, CLIAbort, FormattedItem, listing, ArgumentError, SequentialOutput) +from SoftLayer.CLI.helpers import CLIAbort, ArgumentError, format_output from SoftLayer.CLI.environment import ( Environment, CLIRunnableType, InvalidCommand, InvalidModule) @@ -52,56 +48,6 @@ } -def format_output(data, fmt='table'): - if isinstance(data, basestring): - return data - - if isinstance(data, Table): - if fmt == 'table': - return str(format_prettytable(data)) - elif fmt == 'raw': - return str(format_no_tty(data)) - - if fmt != 'raw' and isinstance(data, FormattedItem): - return str(data.formatted) - - if isinstance(data, SequentialOutput): - output = [format_output(d, fmt=fmt) for d in data] - if not data.blanks: - output = [x for x in output if len(x)] - return format_output(output, fmt=fmt) - - if isinstance(data, list) or isinstance(data, tuple): - output = [format_output(d, fmt=fmt) for d in data] - return format_output(listing(output, separator=os.linesep)) - - return str(data) - - -def format_prettytable(table): - for i, row in enumerate(table.rows): - for j, item in enumerate(row): - table.rows[i][j] = format_output(item) - t = table.prettytable() - t.hrules = FRAME - t.horizontal_char = '.' - t.vertical_char = ':' - t.junction_char = ':' - return t - - -def format_no_tty(table): - t = table.prettytable() - for col in table.columns: - t.align[col] = 'l' - t.hrules = NONE - t.border = False - t.header = False - t.left_padding_width = 0 - t.right_padding_width = 2 - return t - - class CommandParser(object): def __init__(self, env): self.env = env @@ -238,7 +184,7 @@ def main(args=sys.argv[1:], env=Environment()): exit_status = 1 except (ValueError, KeyError): raise - except DocoptExit, e: + except DocoptExit as e: env.err(e.usage) env.err( '\nUnknown argument(s), use -h or --help for available options') @@ -246,15 +192,22 @@ def main(args=sys.argv[1:], env=Environment()): except KeyboardInterrupt: env.out('') exit_status = 1 - except CLIAbort, e: + except CLIAbort as e: env.err(str(e.message)) exit_status = e.code - except SystemExit, e: + except SystemExit as e: exit_status = e.code - except SoftLayerError, e: + except SoftLayerAPIError as e: + if 'invalid api token' in e.faultString.lower(): + env.out("Authentication Failed: To update your credentials, use " + "'sl config setup'") + else: + env.err(str(e)) + exit_status = 1 + except SoftLayerError as e: env.err(str(e)) exit_status = 1 - except Exception, e: + except Exception as e: import traceback env.err(traceback.format_exc()) exit_status = 1 diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 6e6f17d8b..6786d97ce 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -7,6 +7,7 @@ :license: BSD, see LICENSE for more details. """ import sys +import getpass from importlib import import_module from ConfigParser import SafeConfigParser import os @@ -85,6 +86,9 @@ def err(self, s, nl=True): def input(self, prompt): return raw_input(prompt) + def getpass(self, prompt): + return getpass.getpass(prompt) + def load_config(self, files): config_files = [os.path.expanduser(f) for f in files] diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index b2083b940..afe3e4530 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -6,13 +6,65 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ +import os + from SoftLayer.CLI.environment import CLIRunnableType from SoftLayer.utils import NestedDict -from prettytable import PrettyTable +from prettytable import PrettyTable, FRAME, NONE __all__ = ['Table', 'CLIRunnable', 'FormattedItem', 'valid_response', 'confirm', 'no_going_back', 'mb_to_gb', 'gb', 'listing', 'CLIAbort', - 'NestedDict', 'resolve_id'] + 'NestedDict', 'resolve_id', 'format_output'] + + +def format_output(data, fmt='table'): + if isinstance(data, basestring): + return data + + if isinstance(data, Table): + if fmt == 'table': + return str(format_prettytable(data)) + elif fmt == 'raw': + return str(format_no_tty(data)) + + if fmt != 'raw' and isinstance(data, FormattedItem): + return str(data.formatted) + + if isinstance(data, SequentialOutput): + output = [format_output(d, fmt=fmt) for d in data] + if not data.blanks: + output = [x for x in output if len(x)] + return format_output(output, fmt=fmt) + + if isinstance(data, list) or isinstance(data, tuple): + output = [format_output(d, fmt=fmt) for d in data] + return format_output(listing(output, separator=os.linesep)) + + return str(data) + + +def format_prettytable(table): + for i, row in enumerate(table.rows): + for j, item in enumerate(row): + table.rows[i][j] = format_output(item) + t = table.prettytable() + t.hrules = FRAME + t.horizontal_char = '.' + t.vertical_char = ':' + t.junction_char = ':' + return t + + +def format_no_tty(table): + t = table.prettytable() + for col in table.columns: + t.align[col] = 'l' + t.hrules = NONE + t.border = False + t.header = False + t.left_padding_width = 0 + t.right_padding_width = 2 + return t class FormattedItem(object): diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index 12870f184..958d63b79 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -12,10 +12,49 @@ import os.path -from SoftLayer.CLI import CLIRunnable, CLIAbort, Table, confirm +from SoftLayer import Client, SoftLayerAPIError +from SoftLayer.CLI import CLIRunnable, CLIAbort, Table, confirm, format_output import ConfigParser +def config_table(env): + t = Table(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + config = env.config + t.add_row(['Username', config.get('username', 'none set')]) + t.add_row(['API Key', config.get('api_key', 'none set')]) + t.add_row(['Endpoint URL', config.get('endpoint_url', 'none set')]) + return t + + +def get_api_key(username, secret, endpoint_url=None): + + # Try to use a client with username/api key + try: + client = Client( + username=username, + api_key=secret, + endpoint_url=endpoint_url) + + client['Account'].getCurrentUser() + return secret + except SoftLayerAPIError as e: + if 'invalid api token' not in e.faultString.lower(): + raise + + # Try to use a client with username/password + client = Client(endpoint_url=endpoint_url) + client.authenticate_with_password(username, secret) + + account = client['Account'].getCurrentUser( + mask='apiAuthenticationKeys') + api_keys = account['apiAuthenticationKeys'] + if len(api_keys) == 0: + return client['User_Customer'].addApiAuthenticationKey() + return api_keys[0]['authenticationKey'] + + class Setup(CLIRunnable): """ usage: sl config setup [options] @@ -27,7 +66,7 @@ class Setup(CLIRunnable): @classmethod def execute(cls, client, args): username = cls.env.input('Username: ') - api_key = cls.env.input('API Key: ') + secret = cls.env.getpass('API Key or Password: ') endpoint_url = cls.env.input( 'Endpoint URL [%s]: ' % cls.env.config['endpoint_url']) if not endpoint_url: @@ -36,25 +75,26 @@ def execute(cls, client, args): path = '~/.softlayer' if args.get('--config'): path = args.get('--config') - config_path = os.path.expanduser(path) - c = confirm( - 'Are you sure you want to write settings to "%s"?' % config_path) - if not c: + api_key = get_api_key(username, secret, endpoint_url=endpoint_url) + + cls.env.config['username'] = username + cls.env.config['api_key'] = api_key + cls.env.config['endpoint_url'] = endpoint_url + + cls.env.out(format_output(config_table(cls.env))) + + if not confirm('Are you sure you want to write settings to "%s"?' + % config_path, default=True): raise CLIAbort('Aborted.') config = ConfigParser.RawConfigParser() config.add_section('softlayer') - if username: - config.set('softlayer', 'username', username) - - if api_key: - config.set('softlayer', 'api_key', api_key) - - if endpoint_url: - config.set('softlayer', 'endpoint_url', endpoint_url) + config.set('softlayer', 'username', cls.env.config['username']) + config.set('softlayer', 'api_key', cls.env.config['api_key']) + config.set('softlayer', 'endpoint_url', cls.env.config['endpoint_url']) f = os.fdopen( os.open(config_path, os.O_WRONLY | os.O_CREAT, 0600), 'w') @@ -74,11 +114,4 @@ class Show(CLIRunnable): @classmethod def execute(cls, client, args): - t = Table(['Name', 'Value']) - t.align['Name'] = 'r' - t.align['Value'] = 'l' - config = cls.env.config - t.add_row(['Username', config.get('username', 'none set')]) - t.add_row(['API Key', config.get('api_key', 'none set')]) - t.add_row(['Endpoint URL', config.get('endpoint_url', 'none set')]) - return t + return config_table(cls.env) diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index d1634ebea..9a2fb81e8 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -5,8 +5,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import os -import os.path try: import unittest2 as unittest except ImportError: @@ -116,6 +114,19 @@ def test_softlayer_error(self): self.assertRaises( SystemExit, cli.core.main, args=['cci', 'list'], env=self.env) + def test_softlayer_api_error(self): + error = SoftLayer.SoftLayerAPIError('Exception', 'Exception Text') + self.env.get_module_name.side_effect = error + self.assertRaises( + SystemExit, cli.core.main, args=['cci', 'list'], env=self.env) + + def test_softlayer_api_error_authentication_error(self): + error = SoftLayer.SoftLayerAPIError('SoftLayerException', + 'Invalid API Token') + self.env.get_module_name.side_effect = error + self.assertRaises( + SystemExit, cli.core.main, args=['cci', 'list'], env=self.env) + def test_system_exit_error(self): self.env.get_module_name.side_effect = SystemExit self.assertRaises( @@ -203,59 +214,3 @@ def test_confirm(self): self.env.get_command.return_value = command self.assertRaises( SystemExit, self.parser.parse_command_args, 'cci', 'list', []) - - -class TestFormatOutput(unittest.TestCase): - def test_format_output_string(self): - t = cli.core.format_output('just a string', 'raw') - self.assertEqual('just a string', t) - - t = cli.core.format_output(u'just a string', 'raw') - self.assertEqual(u'just a string', t) - - def test_format_output_raw(self): - t = cli.Table(['nothing']) - t.align['nothing'] = 'c' - t.add_row(['testdata']) - t.sortby = 'nothing' - ret = cli.core.format_output(t, 'raw') - - self.assertNotIn('nothing', str(ret)) - self.assertIn('testdata', str(ret)) - - def test_format_output_formatted_item(self): - item = cli.FormattedItem('test', 'test_formatted') - ret = cli.core.format_output(item, 'table') - self.assertEqual('test_formatted', ret) - - def test_format_output_list(self): - item = ['this', 'is', 'a', 'list'] - ret = cli.core.format_output(item, 'table') - self.assertEqual(os.linesep.join(item), ret) - - def test_format_output_table(self): - t = cli.Table(['nothing']) - t.align['nothing'] = 'c' - t.add_row(['testdata']) - t.sortby = 'nothing' - ret = cli.core.format_output(t, 'table') - - self.assertIn('nothing', str(ret)) - self.assertIn('testdata', str(ret)) - - def test_unknown(self): - t = cli.core.format_output({}, 'raw') - self.assertEqual('{}', t) - - def test_sequentialoutput(self): - t = cli.core.SequentialOutput(blanks=False) - self.assertTrue(hasattr(t, 'append')) - t.append('This is a test') - t.append('') - t.append('More tests') - output = cli.core.format_output(t) - self.assertEqual("This is a test\nMore tests", output) - - t.blanks = True - output = cli.core.format_output(t) - self.assertEqual("This is a test\n\nMore tests", output) diff --git a/SoftLayer/tests/CLI/environment_tests.py b/SoftLayer/tests/CLI/environment_tests.py index 109849929..0c1ec5c71 100644 --- a/SoftLayer/tests/CLI/environment_tests.py +++ b/SoftLayer/tests/CLI/environment_tests.py @@ -51,6 +51,12 @@ def test_input(self, raw_input_mock): raw_input_mock.assert_called_with('input') self.assertEqual(raw_input_mock(), r) + @patch('getpass.getpass') + def test_getpass(self, getpass): + r = self.env.getpass('input') + getpass.assert_called_with('input') + self.assertEqual(getpass(), r) + @patch('os.environ', {}) def test_parse_config_no_files(self): self.env.load_config([]) diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index fef62c6a0..11911be16 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -6,6 +6,7 @@ :license: BSD, see LICENSE for more details. """ import sys +import os try: import unittest2 as unittest except ImportError: @@ -175,3 +176,59 @@ def test_resolve_id_multiple(self): resolver = lambda r: [12345, 54321] self.assertRaises( cli.helpers.CLIAbort, cli.helpers.resolve_id, resolver, 'test') + + +class TestFormatOutput(unittest.TestCase): + def test_format_output_string(self): + t = cli.helpers.format_output('just a string', 'raw') + self.assertEqual('just a string', t) + + t = cli.helpers.format_output(u'just a string', 'raw') + self.assertEqual(u'just a string', t) + + def test_format_output_raw(self): + t = cli.Table(['nothing']) + t.align['nothing'] = 'c' + t.add_row(['testdata']) + t.sortby = 'nothing' + ret = cli.helpers.format_output(t, 'raw') + + self.assertNotIn('nothing', str(ret)) + self.assertIn('testdata', str(ret)) + + def test_format_output_formatted_item(self): + item = cli.FormattedItem('test', 'test_formatted') + ret = cli.helpers.format_output(item, 'table') + self.assertEqual('test_formatted', ret) + + def test_format_output_list(self): + item = ['this', 'is', 'a', 'list'] + ret = cli.helpers.format_output(item, 'table') + self.assertEqual(os.linesep.join(item), ret) + + def test_format_output_table(self): + t = cli.Table(['nothing']) + t.align['nothing'] = 'c' + t.add_row(['testdata']) + t.sortby = 'nothing' + ret = cli.helpers.format_output(t, 'table') + + self.assertIn('nothing', str(ret)) + self.assertIn('testdata', str(ret)) + + def test_unknown(self): + t = cli.helpers.format_output({}, 'raw') + self.assertEqual('{}', t) + + def test_sequentialoutput(self): + t = cli.helpers.SequentialOutput(blanks=False) + self.assertTrue(hasattr(t, 'append')) + t.append('This is a test') + t.append('') + t.append('More tests') + output = cli.helpers.format_output(t) + self.assertEqual("This is a test\nMore tests", output) + + t.blanks = True + output = cli.helpers.format_output(t) + self.assertEqual("This is a test\n\nMore tests", output) From 4805e1496dde45e14782637496366389f2c8f1a8 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Mon, 22 Jul 2013 15:26:40 -0500 Subject: [PATCH 003/168] Make setup more sane around 2to3 --- setup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index e9992e716..ac49b7d20 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,12 @@ +import sys +import os + try: from setuptools import setup except ImportError: - from distutils.core import setup # NOQA -import sys -import os + print("Distribute is required for install:") + print(" http://python-distribute.org/distribute_setup.py") + sys.exit(1) # Not supported for Python versions < 2.6 if sys.version_info <= (2, 6): @@ -16,7 +19,6 @@ extra['use_2to3'] = True requires = [ - 'distribute', 'prettytable >= 0.7.0', 'docopt == 0.6.1', 'requests' From 7cb0171a63fdfeeed06cf65f5c5db8afc38b7f87 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 24 Jul 2013 09:05:24 -0500 Subject: [PATCH 004/168] CLI: Bug Fix + slightly easier configuration When using 'sl config setup' you can now type in 'public', 'private', or type your own endpoint URL. Fixes a bug where if you don't have an API key it would fail to generate. Now it should properly generate an API key for you if you don't have one when using 'sl config setup' --- SoftLayer/CLI/modules/config.py | 36 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index 958d63b79..8a79213ea 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -12,7 +12,8 @@ import os.path -from SoftLayer import Client, SoftLayerAPIError +from SoftLayer import ( + Client, SoftLayerAPIError, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) from SoftLayer.CLI import CLIRunnable, CLIAbort, Table, confirm, format_output import ConfigParser @@ -47,11 +48,12 @@ def get_api_key(username, secret, endpoint_url=None): client = Client(endpoint_url=endpoint_url) client.authenticate_with_password(username, secret) - account = client['Account'].getCurrentUser( - mask='apiAuthenticationKeys') - api_keys = account['apiAuthenticationKeys'] + user_record = client['Account'].getCurrentUser( + mask='id, apiAuthenticationKeys') + api_keys = user_record['apiAuthenticationKeys'] if len(api_keys) == 0: - return client['User_Customer'].addApiAuthenticationKey() + return client['User_Customer'].addApiAuthenticationKey( + id=user_record['id']) return api_keys[0]['authenticationKey'] @@ -65,12 +67,28 @@ class Setup(CLIRunnable): @classmethod def execute(cls, client, args): - username = cls.env.input('Username: ') - secret = cls.env.getpass('API Key or Password: ') + # User Input + username = cls.env.input( + 'Username [%s]: ' % cls.env.config['username']) \ + or cls.env.config['username'] + secret = cls.env.getpass( + 'API Key or Password [%s]: ' % cls.env.config['api_key']) \ + or cls.env.config['api_key'] + + cls.env.out("Endpoint URL specifies which endpoint will be used " + "during communication with the SLAPI. The default address " + "is accessible over the internet and will work in most " + "cases. You may also type 'private' to use the private " + "network or specify a custom URL.") endpoint_url = cls.env.input( - 'Endpoint URL [%s]: ' % cls.env.config['endpoint_url']) + 'Endpoint URL [%s]: ' + % cls.env.config['endpoint_url']) or cls.env.config['endpoint_url'] if not endpoint_url: endpoint_url = cls.env.config['endpoint_url'] + if endpoint_url == 'public': + endpoint_url = API_PUBLIC_ENDPOINT + elif endpoint_url == 'private': + endpoint_url = API_PRIVATE_ENDPOINT path = '~/.softlayer' if args.get('--config'): @@ -103,6 +121,8 @@ def execute(cls, client, args): finally: f.close() + return "Configuration Updated Successfully" + class Show(CLIRunnable): """ From 787d58ca09dafc959e82f20f4b00b3f24e9d7e99 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 25 Jul 2013 14:29:17 -0700 Subject: [PATCH 005/168] Merge pull request #135 from CrackerJackMack/metadata add metadata, hostname, and domain editing for hardware and CCI --- SoftLayer/CLI/modules/cci.py | 46 +++++++++++++++++++++++++++++ SoftLayer/CLI/modules/hardware.py | 49 ++++++++++++++++++++++++++++++- SoftLayer/managers/cci.py | 33 +++++++++++++++++++++ SoftLayer/managers/hardware.py | 34 +++++++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 055cf2d53..e6e7a0279 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -5,6 +5,7 @@ The available commands are: network Manage network settings + edit Edit details of a CCI create Order and create a CCI (see `sl cci create-options` for choices) manage Manage active CCI @@ -762,3 +763,48 @@ def sync_ptr_record(): if both or args.get('--PTR'): sync_ptr_record() + + +class EditCCI(CLIRunnable): + """ +usage: sl cci edit [options] + +Edit CCI details + +Options: + -H --hostname=HOST Host portion of the FQDN. example: server + -D --domain=DOMAIN Domain portion of the FQDN example: example.com + -u --userdata=DATA User defined metadata string + -F --userfile=FILE Read userdata from file +""" + action = 'edit' + + @staticmethod + def execute(client, args): + data = {} + + if args['--userdata'] and args['--userfile']: + raise ArgumentError('[-u | --userdata] not allowed with ' + '[-F | --userfile]') + if args['--userfile']: + if not os.path.exists(args['--userfile']): + raise ArgumentError( + 'File does not exist [-u | --userfile] = %s' + % args['--userfile']) + + if args.get('--userdata'): + data['userdata'] = args['--userdata'] + elif args.get('--userfile'): + f = open(args['--userfile'], 'r') + try: + data['userdata'] = f.read() + finally: + f.close() + + data['hostname'] = args.get('--hostname') + data['domain'] = args.get('--domain') + + cci = CCIManager(client) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') + if not cci.edit(cci_id, **data): + raise CLIAbort("Failed to update CCI") diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 1fbe604f1..71d307c2f 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -19,10 +19,11 @@ hostname or the ip address for a piece of hardware. """ import re +import os from os import linesep from SoftLayer.CLI.helpers import ( CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, - SequentialOutput, gb, no_going_back, resolve_id, confirm) + SequentialOutput, gb, no_going_back, resolve_id, confirm, ArgumentError) from SoftLayer import HardwareManager @@ -746,3 +747,49 @@ def _get_price_id_from_options(cls, ds_options, option, value): price_id = item_options[1] return price_id + + +class EditHardware(CLIRunnable): + """ +usage: sl hardware edit [options] + +Edit hardware details + +Options: + -H --hostname=HOST Host portion of the FQDN. example: server + -D --domain=DOMAIN Domain portion of the FQDN example: example.com + -u --userdata=DATA User defined metadata string + -F --userfile=FILE Read userdata from file +""" + action = 'edit' + + @staticmethod + def execute(client, args): + data = {} + + if args['--userdata'] and args['--userfile']: + raise ArgumentError('[-u | --userdata] not allowed with ' + '[-F | --userfile]') + if args['--userfile']: + if not os.path.exists(args['--userfile']): + raise ArgumentError( + 'File does not exist [-u | --userfile] = %s' + % args['--userfile']) + + if args.get('--userdata'): + data['userdata'] = args['--userdata'] + elif args.get('--userfile'): + f = open(args['--userfile'], 'r') + try: + data['userdata'] = f.read() + finally: + f.close() + + data['hostname'] = args.get('--hostname') + data['domain'] = args.get('--domain') + + hw = HardwareManager(client) + hw_id = resolve_id(hw.resolve_ids, args.get(''), + 'hardware') + if not hw.edit(hw_id, **data): + raise CLIAbort("Failed to update hardware") diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 54a5b571f..21ccc24cc 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -143,6 +143,7 @@ def get_instance(self, id, **kwargs): 'activeTransaction.id', 'blockDevices', 'blockDeviceTemplateGroup[id, name]', + 'userData', 'status.name', 'operatingSystem.softwareLicense.' 'softwareDescription[manufacturer,name,version,referenceCode]', @@ -311,3 +312,35 @@ def _get_ids_from_ip(self, ip): results = self.list_instances(private_ip=ip, mask="id") if results: return [result['id'] for result in results] + + def edit(self, id, userdata=None, hostname=None, domain=None, notes=None): + """ Edit hostname, domain name, notes, and/or the user data of a CCI + + Parameters set to None will be ignored and not attempted to be updated. + + :param integer id: the instance ID to edit + :param string userdata: user data on CCI to edit. + If none exist it will be created + :param string hostname: valid hostname + :param string domain: valid domain namem + :param string notes: notes about this particular CCI + + """ + + obj = {} + if userdata: + self.guest.setUserMetadata([userdata], id=id) + + if hostname: + obj['hostname'] = hostname + + if domain: + obj['domain'] = domain + + if notes: + obj['notes'] = notes + + if not obj: + return True + + return self.guest.editObject(obj, id=id) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index d51fdfb56..ad2e638b5 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -217,6 +217,7 @@ def get_hardware(self, id, **kwargs): 'notes', 'primaryBackendIpAddress', 'primaryIpAddress', + 'userData', 'datacenter.name', 'networkComponents[id, status, speed, maxSpeed, name,' 'ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,' @@ -476,6 +477,39 @@ def _parse_package_data(self, id): return results + def edit(self, id, userdata=None, hostname=None, domain=None, notes=None): + """ Edit hostname, domain name, notes, and/or the + user data of the hardware + + Parameters set to None will be ignored and not attempted to be updated. + + :param integer id: the instance ID to edit + :param string userdata: user data on the hardware to edit. + If none exist it will be created + :param string hostname: valid hostname + :param string domain: valid domain namem + :param string notes: notes about this particular hardware + + """ + + obj = {} + if userdata: + self.hardware.setUserMetadata([userdata], id=id) + + if hostname: + obj['hostname'] = hostname + + if domain: + obj['domain'] = domain + + if notes: + obj['notes'] = notes + + if not obj: + return True + + return self.hardware.editObject(obj, id=id) + def get_default_value(package_options, category): if category not in package_options['categories']: From d022623c3a87e481a93ae28548894517e8f7e7ac Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 29 Jul 2013 08:32:00 -0500 Subject: [PATCH 006/168] CLI: Adds JSON Output format for most commands * Changed format of displaying users/passwords in cci details (nested tables) * Changed format of displaying cpu/memory options in create-options for cci, hardware and bmetal (nested tables) * Adds confirmation option to 'sl hardware create' * Creates a to_python() standard for pythonizable types * For CCI/Hardware listing/detail datacenters shows as 'San Jose 1' for table output and 'sjc01' for JSON and raw outputs * For CCI status/state detail shows as 'Active'/'Running' for table output and as 'ACTIVE'/'RUNNING' for JSON and raw outputs --- SoftLayer/CLI/core.py | 4 +- SoftLayer/CLI/helpers.py | 160 +++++++++++++++++++++------- SoftLayer/CLI/modules/bmetal.py | 35 +++--- SoftLayer/CLI/modules/cci.py | 31 +++--- SoftLayer/CLI/modules/config.py | 5 +- SoftLayer/CLI/modules/hardware.py | 62 ++++++----- SoftLayer/CLI/modules/image.py | 4 +- SoftLayer/CLI/modules/metadata.py | 6 +- SoftLayer/managers/cci.py | 11 +- SoftLayer/managers/hardware.py | 4 +- SoftLayer/tests/CLI/helper_tests.py | 79 +++++++++++++- 11 files changed, 288 insertions(+), 113 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index eecb5bd88..a9b912817 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -47,6 +47,8 @@ '3': logging.DEBUG } +VALID_FORMATS = ['raw', 'table', 'json'] + class CommandParser(object): def __init__(self, env): @@ -164,7 +166,7 @@ def main(args=sys.argv[1:], env=Environment()): data = command.execute(client, command_args) if data: format = command_args.get('--format', 'table') - if format not in ['raw', 'table']: + if format not in VALID_FORMATS: raise ArgumentError('Invalid format "%s"' % format) s = format_output(data, fmt=format) if s: diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index afe3e4530..b0a0c3cff 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -7,39 +7,60 @@ :license: BSD, see LICENSE for more details. """ import os +import json from SoftLayer.CLI.environment import CLIRunnableType from SoftLayer.utils import NestedDict from prettytable import PrettyTable, FRAME, NONE -__all__ = ['Table', 'CLIRunnable', 'FormattedItem', 'valid_response', - 'confirm', 'no_going_back', 'mb_to_gb', 'gb', 'listing', 'CLIAbort', - 'NestedDict', 'resolve_id', 'format_output'] +__all__ = ['Table', 'KeyValueTable', 'CLIRunnable', 'FormattedItem', + 'valid_response', 'confirm', 'no_going_back', 'mb_to_gb', 'gb', + 'listing', 'CLIAbort', 'NestedDict', 'resolve_id', 'format_output'] def format_output(data, fmt='table'): + """ Given some data, will format it for output + + :param data: One of: String, Table, FormattedItem, List, Tuple, + SequentialOutput + :param string fmt (optional): One of: table, raw, json, python + """ if isinstance(data, basestring): return data - if isinstance(data, Table): + # responds to .prettytable() + if hasattr(data, 'prettytable'): if fmt == 'table': return str(format_prettytable(data)) elif fmt == 'raw': return str(format_no_tty(data)) - if fmt != 'raw' and isinstance(data, FormattedItem): - return str(data.formatted) + # responds to .to_python() + if hasattr(data, 'to_python'): + if fmt == 'json': + return json.dumps( + format_output(data, fmt='python'), + indent=4, + cls=CLIJSONEncoder) + elif fmt == 'python': + return data.to_python() + + # responds to .formatted + if hasattr(data, 'formatted'): + if fmt == 'table': + return str(data.formatted) - if isinstance(data, SequentialOutput): - output = [format_output(d, fmt=fmt) for d in data] - if not data.blanks: - output = [x for x in output if len(x)] - return format_output(output, fmt=fmt) + # responds to .separator + if hasattr(data, 'separator'): + output = [format_output(d, fmt=fmt) for d in data if d] + return str(SequentialOutput(data.separator, output)) + # is iterable if isinstance(data, list) or isinstance(data, tuple): output = [format_output(d, fmt=fmt) for d in data] return format_output(listing(output, separator=os.linesep)) + # fallback, convert this odd object to a string return str(data) @@ -56,6 +77,9 @@ def format_prettytable(table): def format_no_tty(table): + for i, row in enumerate(table.rows): + for j, item in enumerate(row): + table.rows[i][j] = format_output(item, fmt='raw') t = table.prettytable() for col in table.columns: t.align[col] = 'l' @@ -75,53 +99,51 @@ def __init__(self, original, formatted=None): else: self.formatted = self.original + def to_python(self): + return self.original + def __str__(self): + if self.original is None: + return 'NULL' return str(self.original) __repr__ = __str__ -def resolve_id(resolver, identifier, name='object'): - """ Resolves a single id using an id resolver function which returns a list - of ids. - - :param resolver: function that resolves ids. Should return None or a list - of ids. - :param string identifier: a string identifier used to resolve ids - :param string name: the object type, to be used in error messages +def mb_to_gb(megabytes): + """ Takes in the number of megabytes and returns a FormattedItem that + displays gigabytes. + :param int megabytes: number of megabytes """ - ids = resolver(identifier) - - if len(ids) == 0: - raise CLIAbort("Error: Unable to find %s '%s'" % (name, identifier)) - - if len(ids) > 1: - raise CLIAbort( - "Error: Multiple %s found for '%s': %s" % - (name, identifier, ', '.join([str(_id) for _id in ids]))) - - return ids[0] - - -def mb_to_gb(megabytes): return FormattedItem(megabytes, "%dG" % (float(megabytes) / 1024)) def gb(gigabytes): + """ Takes in the number of gigabytes and returns a FormattedItem that + displays gigabytes. + + :param int gigabytes: number of gigabytes + """ return FormattedItem(int(float(gigabytes)) * 1024, "%dG" % int(float(gigabytes))) def blank(): """ Returns FormatedItem to make pretty output use a dash - and raw formatting to use NULL""" - return FormattedItem('NULL', '-') + and raw formatting to use NULL + """ + return FormattedItem(None, '-') -def listing(item, separator=','): - l = separator.join((str(i) for i in item)) - return FormattedItem(l, l) +def listing(items, separator=','): + """ Given an iterable, returns a FormatedItem which display a list of + items + + :param items: An iterable that outputs strings + :param string separator: the separator to use + """ + return SequentialOutput(separator, items) class CLIRunnable(object): @@ -138,6 +160,29 @@ def execute(client, args): pass +def resolve_id(resolver, identifier, name='object'): + """ Resolves a single id using an id resolver function which returns a list + of ids. + + :param resolver: function that resolves ids. Should return None or a list + of ids. + :param string identifier: a string identifier used to resolve ids + :param string name: the object type, to be used in error messages + + """ + ids = resolver(identifier) + + if len(ids) == 0: + raise CLIAbort("Error: Unable to find %s '%s'" % (name, identifier)) + + if len(ids) > 1: + raise CLIAbort( + "Error: Multiple %s found for '%s': %s" % + (name, identifier, ', '.join([str(_id) for _id in ids]))) + + return ids[0] + + def valid_response(prompt, *valid): ans = raw_input(prompt).lower() @@ -202,6 +247,19 @@ def __init__(self, columns): def add_row(self, row): self.rows.append(row) + def _format_python_value(self, value): + if hasattr(value, 'to_python'): + return value.to_python() + return value + + def to_python(self): + # Adding rows + l = [] + for row in self.rows: + formatted_row = [self._format_python_value(v) for v in row] + l.append(dict(zip(self.columns, formatted_row))) + return l + def prettytable(self): """ Returns a new prettytable instance. """ t = PrettyTable(self.columns) @@ -216,6 +274,28 @@ def prettytable(self): return t +class KeyValueTable(Table): + def to_python(self): + d = {} + for row in self.rows: + d[row[0]] = self._format_python_value(row[1]) + return d + + class SequentialOutput(list): - def __init__(self, blanks=True, *args, **kwargs): - self.blanks = blanks + def __init__(self, separator=os.linesep, *args, **kwargs): + self.separator = separator + super(SequentialOutput, self).__init__(*args, **kwargs) + + def to_python(self): + return self + + def __str__(self): + return self.separator.join(str(x) for x in self) + + +class CLIJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, FormattedItem): + return obj.to_python() + return super(CLIJSONEncoder, self).default(obj) diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 89b69569b..1c3ea0b0e 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -15,7 +15,8 @@ import re from os import linesep from SoftLayer.CLI import ( - CLIRunnable, Table, no_going_back, confirm, listing, FormattedItem) + CLIRunnable, Table, KeyValueTable, no_going_back, confirm, listing, + FormattedItem) from SoftLayer.CLI.helpers import (CLIAbort, SequentialOutput) from SoftLayer import HardwareManager @@ -54,7 +55,7 @@ class BMetalCreateOptions(CLIRunnable): @classmethod def execute(cls, client, args): - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' @@ -78,31 +79,39 @@ def execute(cls, client, args): if args['--cpu'] or args['--memory'] or show_all: results = cls.get_create_options(bmi_options, 'cpu') - + memory_cpu_table = Table(['memory', 'cpu']) for result in results: - t.add_row([result[0], listing( - item[0] for item in sorted(result[1], - key=lambda x: int(x[0])))]) + memory_cpu_table.add_row([ + result[0], + listing( + [item[0] for item in sorted( + result[1], key=lambda x: int(x[0]) + )])]) + t.add_row(['memory/cpu', memory_cpu_table]) if args['--os'] or show_all: results = cls.get_create_options(bmi_options, 'os') for result in results: - t.add_row([result[0], linesep.join( - item[0] for item in sorted(result[1]))]) + t.add_row([ + result[0], + listing( + [item[0] for item in sorted(result[1])], + separator=linesep + )]) if args['--disk'] or show_all: results = cls.get_create_options(bmi_options, 'disk')[0] t.add_row([results[0], listing( - item[0] for item in sorted(results[1]))]) + [item[0] for item in sorted(results[1])])]) if args['--nic'] or show_all: results = cls.get_create_options(bmi_options, 'nic') for result in results: t.add_row([result[0], listing( - item[0] for item in sorted(result[1],))]) + [item[0] for item in sorted(result[1],)])]) return t @@ -143,7 +152,7 @@ def get_create_options(cls, bmi_options, section, pretty=True): key = memory if pretty: - key = 'cpus (%s gb ram)' % memory + key = memory results.append((key, mem_options[memory])) @@ -389,7 +398,7 @@ def execute(cls, client, args): if args.get('--hourly'): billing_rate = 'hourly' t.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) - output = SequentialOutput(blanks=False) + output = SequentialOutput() output.append(t) output.append(FormattedItem( '', @@ -400,7 +409,7 @@ def execute(cls, client, args): "This action will incur charges on your account. Continue?"): result = mgr.place_order(**order) - t = Table(['name', 'value']) + t = KeyValueTable(['name', 'value']) t.align['name'] = 'r' t.align['value'] = 'l' t.add_row(['id', result['orderId']]) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index e6e7a0279..7dd084e7b 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -31,7 +31,8 @@ CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, FormattedItem) from SoftLayer.CLI.helpers import ( - CLIAbort, ArgumentError, SequentialOutput, NestedDict, blank, resolve_id) + CLIAbort, ArgumentError, SequentialOutput, NestedDict, blank, resolve_id, + KeyValueTable) class ListCCIs(CLIRunnable): @@ -97,7 +98,8 @@ def execute(client, args): guest = NestedDict(guest) t.add_row([ guest['id'], - guest['datacenter']['name'] or blank(), + FormattedItem(guest['datacenter']['name'], + guest['datacenter']['longName']), guest['fullyQualifiedDomainName'], guest['maxCpu'], mb_to_gb(guest['maxMemory']), @@ -125,8 +127,7 @@ class CCIDetails(CLIRunnable): @staticmethod def execute(client, args): cci = CCIManager(client) - - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' @@ -136,9 +137,12 @@ def execute(client, args): t.add_row(['id', result['id']]) t.add_row(['hostname', result['fullyQualifiedDomainName']]) - t.add_row(['status', result['status']['name']]) - t.add_row(['state', result['powerState']['name']]) - t.add_row(['datacenter', result['datacenter']['name'] or blank()]) + t.add_row(['status', FormattedItem( + result['status']['keyName'], result['status']['name'])]) + t.add_row(['state', FormattedItem( + result['powerState']['keyName'], result['powerState']['name'])]) + t.add_row(['datacenter', FormattedItem( + result['datacenter']['name'], result['datacenter']['longName'])]) t.add_row(['cores', result['maxCpu']]) t.add_row(['memory', mb_to_gb(result['maxMemory'])]) t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) @@ -163,11 +167,10 @@ def execute(client, args): t.add_row(['price rate', result['billingItem']['recurringFee']]) if args.get('--passwords'): - user_strs = [] + pass_table = Table(['username', 'password']) for item in result['operatingSystem']['passwords']: - user_strs.append( - "%s %s" % (item['username'], item['password'])) - t.add_row(['users', listing(user_strs)]) + pass_table.add_row([item['username'], item['password']]) + t.add_row(['users', pass_table]) tag_row = [] for tag in result['tagReferences']: @@ -218,7 +221,7 @@ def execute(cls, client, args): if args['--all']: show_all = True - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' @@ -443,7 +446,7 @@ def execute(client, args): if args.get('--hourly'): billing_rate = 'hourly' t.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) - output = SequentialOutput(blanks=False) + output = SequentialOutput() output.append(t) output.append(FormattedItem( '', @@ -455,7 +458,7 @@ def execute(client, args): "This action will incur charges on your account. Continue?"): result = cci.create_instance(**data) - t = Table(['name', 'value']) + t = KeyValueTable(['name', 'value']) t.align['name'] = 'r' t.align['value'] = 'l' t.add_row(['id', result['id']]) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index 8a79213ea..21e2f5249 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -14,12 +14,13 @@ from SoftLayer import ( Client, SoftLayerAPIError, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) -from SoftLayer.CLI import CLIRunnable, CLIAbort, Table, confirm, format_output +from SoftLayer.CLI import ( + CLIRunnable, CLIAbort, KeyValueTable, confirm, format_output) import ConfigParser def config_table(env): - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' config = env.config diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 71d307c2f..aa274fb7a 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -22,8 +22,9 @@ import os from os import linesep from SoftLayer.CLI.helpers import ( - CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, - SequentialOutput, gb, no_going_back, resolve_id, confirm, ArgumentError) + CLIRunnable, Table, KeyValueTable, FormattedItem, NestedDict, CLIAbort, + blank, listing, SequentialOutput, gb, no_going_back, resolve_id, confirm, + ArgumentError) from SoftLayer import HardwareManager @@ -88,7 +89,8 @@ def execute(client, args): server = NestedDict(server) t.add_row([ server['id'], - server['datacenter']['name'] or blank(), + FormattedItem(server['datacenter']['name'], + server['datacenter']['longName']), server['fullyQualifiedDomainName'], server['processorCoreAmount'], gb(server['memoryCapacity']), @@ -115,7 +117,7 @@ class HardwareDetails(CLIRunnable): def execute(client, args): hardware = HardwareManager(client) - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' @@ -127,7 +129,9 @@ def execute(client, args): t.add_row(['id', result['id']]) t.add_row(['hostname', result['fullyQualifiedDomainName']]) t.add_row(['status', result['hardwareStatus']['status']]) - t.add_row(['datacenter', result['datacenter']['name'] or blank()]) + t.add_row(['datacenter', + FormattedItem(result['datacenter']['name'], + result['datacenter']['longName'])]) t.add_row(['cores', result['processorCoreAmount']]) t.add_row(['memory', gb(result['memoryCapacity'])]) t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) @@ -141,7 +145,7 @@ def execute(client, args): result['operatingSystem']['softwareLicense'] ['softwareDescription']['name'] or blank() )]) - t.add_row(['created', result['provisionDate']]) + t.add_row(['created', result['provisionDate'] or blank()]) if result.get('notes'): t.add_row(['notes', result['notes']]) @@ -211,12 +215,12 @@ class CancelHardware(CLIRunnable): action = 'cancel' options = ['confirm'] - @staticmethod - def execute(client, args): + @classmethod + def execute(cls, client, args): hw = HardwareManager(client) hw_id = resolve_id(hw, args.get('')) - print "(Optional) Add a cancellation comment:", + cls.env.out("(Optional) Add a cancellation comment:", nl=False) comment = raw_input() reason = args.get('--reason') @@ -345,7 +349,7 @@ class HardwareCreateOptions(CLIRunnable): def execute(cls, client, args): mgr = HardwareManager(client) - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' @@ -370,9 +374,10 @@ def execute(cls, client, args): if args['--cpu'] or show_all: results = cls.get_create_options(ds_options, 'cpu') + cpu_table = Table(['id', 'description']) for result in sorted(results): - t.add_row([result[0], listing( - item[0] for item in sorted(result[1]))]) + cpu_table.add_row([result[1], result[0]]) + t.add_row(['cpu', cpu_table]) if args['--memory'] or show_all: results = cls.get_create_options(ds_options, 'memory')[0] @@ -384,14 +389,22 @@ def execute(cls, client, args): results = cls.get_create_options(ds_options, 'os') for result in results: - t.add_row([result[0], linesep.join( - item[0] for item in sorted(result[1]))]) + t.add_row([ + result[0], + listing( + [item[0] for item in sorted(result[1])], + separator=linesep + )]) if args['--disk'] or show_all: results = cls.get_create_options(ds_options, 'disk')[0] - t.add_row([results[0], linesep.join( - item[0] for item in sorted(results[1],))]) + t.add_row([ + results[0], + listing( + [item[0] for item in sorted(results[1])], + separator=linesep + )]) if args['--nic'] or show_all: results = cls.get_create_options(ds_options, 'nic') @@ -427,16 +440,12 @@ def get_create_options(cls, ds_options, section, pretty=True): return [('datacenter', datacenters)] elif 'cpu' == section: results = [] - cpu_regex = re.compile('\s(\w+)\s(\d+)\s+\-\s+([\d\.]+GHz)' - '\s+\([\w ]+\)\s+\-\s+(.+)$') for item in ds_options['categories']['server']['items']: - cpu = cpu_regex.search(item['description']) - text = 'cpu: ' + cpu.group(1) + ' ' + cpu.group(2) + ' (' \ - + cpu.group(3) + ', ' + cpu.group(4) + ')' - - if cpu: - results.append((text, [(cpu.group(2), item['price_id'])])) + results.append(( + item['description'], + item['price_id'] + )) return results elif 'memory' == section: @@ -612,6 +621,7 @@ class CreateHardware(CLIRunnable): --dry-run, --test Do not create the server, just get a quote """ action = 'create' + options = ['confirm'] @classmethod def execute(cls, client, args): @@ -699,7 +709,7 @@ def execute(cls, client, args): if args.get('--hourly'): billing_rate = 'hourly' t.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) - output = SequentialOutput(blanks=False) + output = SequentialOutput() output.append(t) output.append(FormattedItem( '', @@ -710,7 +720,7 @@ def execute(cls, client, args): "This action will incur charges on your account. Continue?"): result = mgr.place_order(**order) - t = Table(['name', 'value']) + t = KeyValueTable(['name', 'value']) t.align['name'] = 'r' t.align['value'] = 'l' t.add_row(['id', result['orderId']]) diff --git a/SoftLayer/CLI/modules/image.py b/SoftLayer/CLI/modules/image.py index 45b27ee79..4623750d1 100644 --- a/SoftLayer/CLI/modules/image.py +++ b/SoftLayer/CLI/modules/image.py @@ -4,7 +4,7 @@ Manage compute and flex images The available commands are: - list List active vlans with firewalls + list List images """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. @@ -17,7 +17,7 @@ class ListImages(CLIRunnable): """ usage: sl image list [--public | --private] [options] -List images on the account +List images Options: --public Display only public images diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index f70757687..dfee6893b 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -24,7 +24,7 @@ # :license: BSD, see LICENSE for more details. from SoftLayer import MetadataManager -from SoftLayer.CLI import CLIRunnable, Table, listing, CLIAbort +from SoftLayer.CLI import CLIRunnable, KeyValueTable, listing, CLIAbort class BackendMacAddresses(CLIRunnable): @@ -200,7 +200,7 @@ class Network(CLIRunnable): def execute(client, args): meta = MetadataManager() if args['']: - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' network = meta.public_network() @@ -217,7 +217,7 @@ def execute(client, args): return t if args['']: - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' network = meta.private_network() diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 21ccc24cc..47aaad6b9 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -52,12 +52,12 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, 'primaryBackendIpAddress', 'primaryIpAddress', 'lastKnownPowerState.name', - 'powerState.name', + 'powerState', 'maxCpu', 'maxMemory', - 'datacenter.name', + 'datacenter', 'activeTransaction.transactionStatus[friendlyName,name]', - 'status.name', + 'status', ]) kwargs['mask'] = "mask[%s]" % ','.join(items) @@ -136,15 +136,14 @@ def get_instance(self, id, **kwargs): 'networkComponents[id, status, speed, maxSpeed, name,' 'macAddress, primaryIpAddress, port, primarySubnet]', 'lastKnownPowerState.name', - 'powerState.name', + 'powerState', 'maxCpu', 'maxMemory', - 'datacenter.name', + 'datacenter', 'activeTransaction.id', 'blockDevices', 'blockDeviceTemplateGroup[id, name]', 'userData', - 'status.name', 'operatingSystem.softwareLicense.' 'softwareDescription[manufacturer,name,version,referenceCode]', 'operatingSystem.passwords[username,password]', diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index ad2e638b5..c0544eb35 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -101,7 +101,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, 'memoryCapacity', 'primaryBackendIpAddress', 'primaryIpAddress', - 'datacenter.name', + 'datacenter', ]) kwargs['mask'] = "mask[%s]" % ','.join(items) @@ -218,7 +218,7 @@ def get_hardware(self, id, **kwargs): 'primaryBackendIpAddress', 'primaryIpAddress', 'userData', - 'datacenter.name', + 'datacenter', 'networkComponents[id, status, speed, maxSpeed, name,' 'ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,' 'port, primarySubnet]', diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 11911be16..da8f808c3 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -7,6 +7,7 @@ """ import sys import os +import json try: import unittest2 as unittest except ImportError: @@ -21,6 +22,22 @@ raw_input_path = '__builtin__.raw_input' +class CLIJSONEncoderTest(unittest.TestCase): + def test_default(self): + out = json.dumps({ + 'formattedItem': cli.helpers.FormattedItem('normal', 'formatted') + }, cls=cli.helpers.CLIJSONEncoder) + self.assertEqual(out, '{"formattedItem": "normal"}') + + out = json.dumps({'normal': 'string'}, cls=cli.helpers.CLIJSONEncoder) + self.assertEqual(out, '{"normal": "string"}') + + def test_fail(self): + self.assertRaises( + TypeError, + json.dumps, {'test': object()}, cls=cli.helpers.CLIJSONEncoder) + + class PromptTests(unittest.TestCase): @patch(raw_input_path) @@ -134,8 +151,37 @@ def test_gb(self): def test_blank(self): item = cli.helpers.blank() - self.assertEqual('NULL', item.original) + self.assertEqual(None, item.original) self.assertEqual('-', item.formatted) + self.assertEqual('NULL', str(item)) + + +class FormattedListTests(unittest.TestCase): + def test_init(self): + l = cli.listing([1, 'two'], separator=':') + self.assertEqual([1, 'two'], list(l)) + self.assertEqual(':', l.separator) + + l = cli.listing([]) + self.assertEqual(',', l.separator) + + def test_to_python(self): + l = cli.listing([1, 'two']) + result = l.to_python() + self.assertEqual([1, 'two'], result) + + l = cli.listing(x for x in [1, 'two']) + result = l.to_python() + self.assertEqual([1, 'two'], result) + + def test_str(self): + l = cli.listing([1, 'two']) + result = str(l) + self.assertEqual('1,two', result) + + l = cli.listing((x for x in [1, 'two']), separator=':') + result = str(l) + self.assertEqual('1:two', result) class CLIAbortTests(unittest.TestCase): @@ -196,6 +242,31 @@ def test_format_output_raw(self): self.assertNotIn('nothing', str(ret)) self.assertIn('testdata', str(ret)) + def test_format_output_json(self): + t = cli.Table(['nothing']) + t.align['nothing'] = 'c' + t.add_row(['testdata']) + t.add_row([cli.helpers.blank()]) + t.sortby = 'nothing' + ret = cli.helpers.format_output(t, 'json') + self.assertEqual('''[ + { + "nothing": "testdata" + }, + { + "nothing": null + } +]''', ret) + + def test_format_output_json_keyvaluetable(self): + t = cli.KeyValueTable(['key', 'value']) + t.add_row(['nothing', cli.helpers.blank()]) + t.sortby = 'nothing' + ret = cli.helpers.format_output(t, 'json') + self.assertEqual('''{ + "nothing": null +}''', ret) + def test_format_output_formatted_item(self): item = cli.FormattedItem('test', 'test_formatted') ret = cli.helpers.format_output(item, 'table') @@ -221,7 +292,7 @@ def test_unknown(self): self.assertEqual('{}', t) def test_sequentialoutput(self): - t = cli.helpers.SequentialOutput(blanks=False) + t = cli.helpers.SequentialOutput() self.assertTrue(hasattr(t, 'append')) t.append('This is a test') t.append('') @@ -229,6 +300,6 @@ def test_sequentialoutput(self): output = cli.helpers.format_output(t) self.assertEqual("This is a test\nMore tests", output) - t.blanks = True + t.separator = ',' output = cli.helpers.format_output(t) - self.assertEqual("This is a test\n\nMore tests", output) + self.assertEqual("This is a test,More tests", output) From 226fd6a69c8d2b302694b976f4915d4aa10b75ac Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 29 Jul 2013 09:43:25 -0500 Subject: [PATCH 007/168] Config Setup Tweaks * API: Rest transport errors are now properly converted to SoftLayerAPIErrors * CLI: Since all unknown exceptions have the traceback printed out, there's no reason to re-raise ValueError and KeyError in core. * CLI: Adds timeout to `sl config setup` client --- SoftLayer/CLI/core.py | 2 -- SoftLayer/CLI/modules/config.py | 29 +++++++++++++++-------------- SoftLayer/tests/CLI/core_tests.py | 9 --------- SoftLayer/transport.py | 24 ++++++++++++------------ 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index eecb5bd88..27fb0b6a8 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -182,8 +182,6 @@ def main(args=sys.argv[1:], env=Environment()): env.err('') env.err(str(e)) exit_status = 1 - except (ValueError, KeyError): - raise except DocoptExit as e: env.err(e.usage) env.err( diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index 8a79213ea..3489800f8 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -36,7 +36,8 @@ def get_api_key(username, secret, endpoint_url=None): client = Client( username=username, api_key=secret, - endpoint_url=endpoint_url) + endpoint_url=endpoint_url, + timeout=5) client['Account'].getCurrentUser() return secret @@ -45,7 +46,7 @@ def get_api_key(username, secret, endpoint_url=None): raise # Try to use a client with username/password - client = Client(endpoint_url=endpoint_url) + client = Client(endpoint_url=endpoint_url, timeout=5) client.authenticate_with_password(username, secret) user_record = client['Account'].getCurrentUser( @@ -75,20 +76,20 @@ def execute(cls, client, args): 'API Key or Password [%s]: ' % cls.env.config['api_key']) \ or cls.env.config['api_key'] - cls.env.out("Endpoint URL specifies which endpoint will be used " - "during communication with the SLAPI. The default address " - "is accessible over the internet and will work in most " - "cases. You may also type 'private' to use the private " - "network or specify a custom URL.") - endpoint_url = cls.env.input( - 'Endpoint URL [%s]: ' - % cls.env.config['endpoint_url']) or cls.env.config['endpoint_url'] - if not endpoint_url: - endpoint_url = cls.env.config['endpoint_url'] - if endpoint_url == 'public': + endpoint_type = cls.env.input('Endpoint (public|private|custom): ') + endpoint_type = endpoint_type.lower() + if endpoint_type == 'public': endpoint_url = API_PUBLIC_ENDPOINT - elif endpoint_url == 'private': + elif endpoint_type == 'private': endpoint_url = API_PRIVATE_ENDPOINT + elif endpoint_type == 'custom' or not endpoint_type: + endpoint_url = API_PRIVATE_ENDPOINT + endpoint_url = cls.env.input( + 'Endpoint URL [%s]: ' % cls.env.config['endpoint_url'] + ) or cls.env.config['endpoint_url'] + else: + raise CLIAbort( + 'Public, Private and Custom are the only valid options.') path = '~/.softlayer' if args.get('--config'): diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 9a2fb81e8..7ff05950f 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -132,15 +132,6 @@ def test_system_exit_error(self): self.assertRaises( SystemExit, cli.core.main, args=['cci', 'list'], env=self.env) - def test_value_key_errors(self): - self.env.get_module_name.side_effect = ValueError - self.assertRaises( - ValueError, cli.core.main, args=['cci', 'list'], env=self.env) - - self.env.get_module_name.side_effect = KeyError - self.assertRaises( - KeyError, cli.core.main, args=['cci', 'list'], env=self.env) - @patch('traceback.format_exc') def test_uncaught_error(self, m): # Exceptions not caught should just Exit diff --git a/SoftLayer/transport.py b/SoftLayer/transport.py index 3b3984f27..0e3304802 100644 --- a/SoftLayer/transport.py +++ b/SoftLayer/transport.py @@ -45,7 +45,7 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, response.raise_for_status() result = xmlrpclib.loads(response.content,)[0][0] return result - except xmlrpclib.Fault, e: + except xmlrpclib.Fault as e: # These exceptions are formed from the XML-RPC spec # http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php error_mapping = { @@ -62,9 +62,9 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, } raise error_mapping.get(e.faultCode, SoftLayerAPIError)( e.faultCode, e.faultString) - except requests.HTTPError, e: + except requests.HTTPError as e: raise TransportError(e.response.status_code, str(e)) - except requests.RequestException, e: + except requests.RequestException as e: raise TransportError(0, str(e)) @@ -77,20 +77,20 @@ def make_rest_api_call(method, url, http_headers=None, timeout=None): :param int timeout: number of seconds to use as a timeout """ log.info('%s %s' % (method, url)) - resp = requests.request(method, url, headers=http_headers, timeout=timeout) try: + resp = requests.request( + method, url, headers=http_headers, timeout=timeout) resp.raise_for_status() - except requests.HTTPError, e: + log.debug(resp.content) + if url.endswith('.json'): + return json.loads(resp.content) + else: + return resp.text + except requests.HTTPError as e: if url.endswith('.json'): content = json.loads(e.response.content) raise SoftLayerAPIError(e.response.status_code, content['error']) else: raise SoftLayerAPIError(e.response.status_code, e.response.text) - except requests.RequestException, e: + except requests.RequestException as e: raise TransportError(0, str(e)) - - log.debug(resp.content) - if url.endswith('.json'): - return json.loads(resp.content) - else: - return resp.text From 00c380ec9785ead6a43364bae4b069fb61fc8ca6 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 29 Jul 2013 10:15:08 -0500 Subject: [PATCH 008/168] Reverts to using the datacenter short name to keep it consistent with filter options --- SoftLayer/CLI/modules/cci.py | 6 ++---- SoftLayer/CLI/modules/hardware.py | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 7dd084e7b..5b69cdf9e 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -98,8 +98,7 @@ def execute(client, args): guest = NestedDict(guest) t.add_row([ guest['id'], - FormattedItem(guest['datacenter']['name'], - guest['datacenter']['longName']), + guest['datacenter']['name'], guest['fullyQualifiedDomainName'], guest['maxCpu'], mb_to_gb(guest['maxMemory']), @@ -141,8 +140,7 @@ def execute(client, args): result['status']['keyName'], result['status']['name'])]) t.add_row(['state', FormattedItem( result['powerState']['keyName'], result['powerState']['name'])]) - t.add_row(['datacenter', FormattedItem( - result['datacenter']['name'], result['datacenter']['longName'])]) + t.add_row(['datacenter', result['datacenter']['name']]) t.add_row(['cores', result['maxCpu']]) t.add_row(['memory', mb_to_gb(result['maxMemory'])]) t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index aa274fb7a..7d2efbb2a 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -89,8 +89,7 @@ def execute(client, args): server = NestedDict(server) t.add_row([ server['id'], - FormattedItem(server['datacenter']['name'], - server['datacenter']['longName']), + server['datacenter']['name'], server['fullyQualifiedDomainName'], server['processorCoreAmount'], gb(server['memoryCapacity']), @@ -129,9 +128,7 @@ def execute(client, args): t.add_row(['id', result['id']]) t.add_row(['hostname', result['fullyQualifiedDomainName']]) t.add_row(['status', result['hardwareStatus']['status']]) - t.add_row(['datacenter', - FormattedItem(result['datacenter']['name'], - result['datacenter']['longName'])]) + t.add_row(['datacenter', result['datacenter']['name']]) t.add_row(['cores', result['processorCoreAmount']]) t.add_row(['memory', gb(result['memoryCapacity'])]) t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) From f70a442de49aa0b41f2cb899d91d1ac3362422c9 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 29 Jul 2013 15:24:02 -0500 Subject: [PATCH 009/168] CLI: `sl config setup` defaults to public endpoint if no input is given --- SoftLayer/CLI/modules/config.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index 3489800f8..435fbb0b9 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -76,20 +76,23 @@ def execute(cls, client, args): 'API Key or Password [%s]: ' % cls.env.config['api_key']) \ or cls.env.config['api_key'] - endpoint_type = cls.env.input('Endpoint (public|private|custom): ') - endpoint_type = endpoint_type.lower() - if endpoint_type == 'public': - endpoint_url = API_PUBLIC_ENDPOINT - elif endpoint_type == 'private': - endpoint_url = API_PRIVATE_ENDPOINT - elif endpoint_type == 'custom' or not endpoint_type: - endpoint_url = API_PRIVATE_ENDPOINT - endpoint_url = cls.env.input( - 'Endpoint URL [%s]: ' % cls.env.config['endpoint_url'] - ) or cls.env.config['endpoint_url'] - else: - raise CLIAbort( - 'Public, Private and Custom are the only valid options.') + while True: + endpoint_type = cls.env.input('Endpoint (public|private|custom): ') + endpoint_type = endpoint_type.lower() + if not endpoint_type: + endpoint_url = API_PUBLIC_ENDPOINT + break + if endpoint_type == 'public': + endpoint_url = API_PUBLIC_ENDPOINT + break + elif endpoint_type == 'private': + endpoint_url = API_PRIVATE_ENDPOINT + break + elif endpoint_type == 'custom': + endpoint_url = cls.env.input( + 'Endpoint URL [%s]: ' % cls.env.config['endpoint_url'] + ) or cls.env.config['endpoint_url'] + break path = '~/.softlayer' if args.get('--config'): From 74ae6b54b42eb2fe246626d7a5942061fe55d853 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 30 Jul 2013 08:36:24 -0500 Subject: [PATCH 010/168] CLI: Retry username and api key/password prompts until non-empty in config setup --- SoftLayer/CLI/modules/config.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index 435fbb0b9..5e1f6bf43 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -30,7 +30,6 @@ def config_table(env): def get_api_key(username, secret, endpoint_url=None): - # Try to use a client with username/api key try: client = Client( @@ -69,12 +68,19 @@ class Setup(CLIRunnable): @classmethod def execute(cls, client, args): # User Input - username = cls.env.input( - 'Username [%s]: ' % cls.env.config['username']) \ - or cls.env.config['username'] - secret = cls.env.getpass( - 'API Key or Password [%s]: ' % cls.env.config['api_key']) \ - or cls.env.config['api_key'] + while True: + username = cls.env.input( + 'Username [%s]: ' % cls.env.config['username']) \ + or cls.env.config['username'] + if username: + break + + while True: + secret = cls.env.getpass( + 'API Key or Password [%s]: ' % cls.env.config['api_key']) \ + or cls.env.config['api_key'] + if secret: + break while True: endpoint_type = cls.env.input('Endpoint (public|private|custom): ') @@ -94,17 +100,17 @@ def execute(cls, client, args): ) or cls.env.config['endpoint_url'] break - path = '~/.softlayer' - if args.get('--config'): - path = args.get('--config') - config_path = os.path.expanduser(path) - api_key = get_api_key(username, secret, endpoint_url=endpoint_url) cls.env.config['username'] = username cls.env.config['api_key'] = api_key cls.env.config['endpoint_url'] = endpoint_url + path = '~/.softlayer' + if args.get('--config'): + path = args.get('--config') + config_path = os.path.expanduser(path) + cls.env.out(format_output(config_table(cls.env))) if not confirm('Are you sure you want to write settings to "%s"?' From e4212b83365eebd74ef92d97b747bbf614f6a998 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 30 Jul 2013 11:04:02 -0500 Subject: [PATCH 011/168] Removed Deprecated APIs --- SoftLayer/API.py | 40 ++----- SoftLayer/deprecated.py | 180 ---------------------------- SoftLayer/tests/api_tests.py | 124 ++----------------- SoftLayer/tests/functional_tests.py | 17 ++- 4 files changed, 28 insertions(+), 333 deletions(-) delete mode 100644 SoftLayer/deprecated.py diff --git a/SoftLayer/API.py b/SoftLayer/API.py index dbcae121b..8e04aa991 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -10,7 +10,6 @@ USER_AGENT from SoftLayer.transport import make_xml_rpc_api_call from SoftLayer.exceptions import SoftLayerError -from SoftLayer.deprecated import DeprecatedClientMixin import os @@ -71,13 +70,9 @@ def __repr__(self): return "" % (self.username) -class Client(DeprecatedClientMixin, object): +class Client(object): """ A SoftLayer API client. - :param service_name: the name of the SoftLayer API service to query - :param integer id: an optional object ID if you're instantiating a - particular SoftLayer_API object. Setting an ID defines this client's - initialization parameter. :param username: an optional API username if you wish to bypass the package's built-in username :param api_key: an optional API key if you wish to bypass the package's @@ -100,11 +95,8 @@ class Client(DeprecatedClientMixin, object): """ _prefix = "SoftLayer_" - def __init__(self, service_name=None, id=None, username=None, api_key=None, + def __init__(self, username=None, api_key=None, endpoint_url=None, timeout=None, auth=None): - self._service_name = service_name - self._headers = {} - self._raw_headers = {} self.auth = auth if self.auth is None: @@ -118,11 +110,6 @@ def __init__(self, service_name=None, id=None, username=None, api_key=None, API_PUBLIC_ENDPOINT).rstrip('/') self.timeout = timeout - super(Client, self).__init__( - service_name=service_name, id=id, username=username, - api_key=api_key, endpoint_url=endpoint_url, timeout=timeout, - auth=auth) - def authenticate_with_password(self, username, password, security_question_id=None, security_question_answer=None): @@ -193,19 +180,8 @@ def call(self, service, method, *args, **kwargs): limit = kwargs.get('limit') offset = kwargs.get('offset', 0) - if not headers and self.auth: - headers = self.auth.get_headers() - - http_headers = { - 'User-Agent': USER_AGENT, - 'Content-Type': 'application/xml', - } - if self._raw_headers: - for name, value in self._raw_headers.items(): - http_headers[name] = value - if raw_headers: - for name, value in raw_headers.items(): - http_headers[name] = value + if self.auth: + headers.update(self.auth.get_headers()) if objectid is not None: headers[service + 'InitParameters'] = {'id': int(objectid)} @@ -221,6 +197,14 @@ def call(self, service, method, *args, **kwargs): 'limit': int(limit), 'offset': int(offset) } + + http_headers = { + 'User-Agent': USER_AGENT, + 'Content-Type': 'application/xml', + } + if raw_headers: + http_headers.update(raw_headers) + uri = '/'.join([self._endpoint_url, service]) return make_xml_rpc_api_call(uri, method, args, headers=headers, diff --git a/SoftLayer/deprecated.py b/SoftLayer/deprecated.py deleted file mode 100644 index 1c236e230..000000000 --- a/SoftLayer/deprecated.py +++ /dev/null @@ -1,180 +0,0 @@ -""" - SoftLayer.deprecated - ~~~~~~~~~~~~~~~~~~~~ - This is where deprecated APIs go for their eternal slumber - - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. - :license: BSD, see LICENSE for more details. -""" -from warnings import warn -from SoftLayer.exceptions import SoftLayerError - - -class DeprecatedClientMixin(): - """ This mixin is to be used in SoftLayer.Client so all of these methods - should be available to the client but are all deprecated. - """ - - def __init__(self, id=None, username=None, api_key=None, **kwargs): - - if id is not None: - warn("The id parameter is deprecated", DeprecationWarning) - self.set_init_parameter(int(id)) - - if username and api_key: - self._headers['authenticate'] = { - 'username': username.strip(), - 'apiKey': api_key.strip(), - } - - def __getattr__(self, name): - """ Attempt a SoftLayer API call. - - Use this as a catch-all so users can call SoftLayer API methods - directly against their client object. If the property or method - relating to their client object doesn't exist then assume the user is - attempting a SoftLayer API call and return a simple function that makes - an XML-RPC call. - - :param name: method name - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - if name in ["__name__", "__bases__"]: - raise AttributeError("'Obj' object has no attribute '%s'" % name) - - def call_handler(*args, **kwargs): - if self._service_name is None: - raise SoftLayerError( - "Service is not set on Client instance.") - kwargs['headers'] = self._headers - return self.call(self._service_name, name, *args, **kwargs) - return call_handler - - def add_raw_header(self, name, value): - """ Set HTTP headers for API calls. - - :param name: the header name - :param value: the header value - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - self._raw_headers[name] = value - - def add_header(self, name, value): - """ Set a SoftLayer API call header. - - :param name: the header name - :param value: the header value - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - name = name.strip() - if name is None or name == '': - raise SoftLayerError('Please specify a header name.') - - self._headers[name] = value - - def remove_header(self, name): - """ Remove a SoftLayer API call header. - - :param name: the header name - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - if name in self._headers: - del self._headers[name.strip()] - - def set_authentication(self, username, api_key): - """ Set user and key to authenticate a SoftLayer API call. - - Use this method if you wish to bypass the API_USER and API_KEY class - constants and set custom authentication per API call. - - See https://manage.softlayer.com/Administrative/apiKeychain for more - information. - - :param username: the username to authenticate with - :param api_key: the user's API key - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - self.add_header('authenticate', { - 'username': username.strip(), - 'apiKey': api_key.strip(), - }) - - def set_init_parameter(self, id): - """ Set an initialization parameter header. - - Initialization parameters instantiate a SoftLayer API service object to - act upon during your API method call. For instance, if your account has - a server with ID number 1234, then setting an initialization parameter - of 1234 in the SoftLayer_Hardware_Server Service instructs the API to - act on server record 1234 in your method calls. - - See http://sldn.softlayer.com/article/Using-Initialization-Parameters-SoftLayer-API # NOQA - for more information. - - :param id: the ID of the SoftLayer API object to instantiate - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - self.add_header(self._service_name + 'InitParameters', { - 'id': int(id) - }) - - def set_object_mask(self, mask): - """ Set an object mask to a SoftLayer API call. - - Use an object mask to retrieve data related your API call's result. - Object masks are skeleton objects, or strings that define nested - relational properties to retrieve along with an object's local - properties. See - http://sldn.softlayer.com/article/Using-Object-Masks-SoftLayer-API - for more information. - - :param mask: the object mask you wish to define - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - header = 'SoftLayer_ObjectMask' - - if isinstance(mask, dict): - header = '%sObjectMask' % self._service_name - - self.add_header(header, {'mask': mask}) - - def set_result_limit(self, limit, offset=0): - """ Set a result limit on a SoftLayer API call. - - Many SoftLayer API methods return a group of results. These methods - support a way to limit the number of results retrieved from the - SoftLayer API in a way akin to an SQL LIMIT statement. - - :param limit: the number of results to limit a SoftLayer API call to - :param offset: An optional offset at which to begin a SoftLayer API - call's returned result - - .. deprecated:: 2.0.0 - - """ - warn("deprecated", DeprecationWarning) - self.add_header('resultLimit', { - 'limit': int(limit), - 'offset': int(offset) - }) diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index d63248491..7b1fb66b8 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -10,7 +10,7 @@ except ImportError: import unittest # NOQA -from mock import patch, MagicMock, call +from mock import patch, call import SoftLayer import SoftLayer.API @@ -19,45 +19,27 @@ class Inititialization(unittest.TestCase): def test_init(self): - client = SoftLayer.Client('SoftLayer_User_Customer', - username='doesnotexist', + client = SoftLayer.Client(username='doesnotexist', api_key='issurelywrong', timeout=10) - self.assertEquals(client._service_name, - 'SoftLayer_User_Customer') - self.assertEquals(client._headers, { - 'authenticate': { - 'username': 'doesnotexist', - 'apiKey': 'issurelywrong' - } - }) + self.assertEquals(client.auth.username, 'doesnotexist') + self.assertEquals(client.auth.api_key, 'issurelywrong') self.assertEquals(client._endpoint_url, SoftLayer.API_PUBLIC_ENDPOINT.rstrip('/')) - def test_init_w_id(self): - client = SoftLayer.Client('SoftLayer_User_Customer', 1, - 'doesnotexist', 'issurelywrong') - - self.assertEquals(client._headers, { - 'SoftLayer_User_CustomerInitParameters': {'id': 1}, - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}}) - @patch.dict('os.environ', { 'SL_USERNAME': 'test_user', 'SL_API_KEY': 'test_api_key'}) def test_env(self): client = SoftLayer.Client() - self.assertEquals(client._headers, { - 'authenticate': { - 'username': 'test_user', 'apiKey': 'test_api_key'}}) + self.assertEquals(client.auth.username, 'test_user') + self.assertEquals(client.auth.api_key, 'test_api_key') @patch('SoftLayer.API.API_USERNAME', 'test_user') @patch('SoftLayer.API.API_KEY', 'test_api_key') def test_globals(self): client = SoftLayer.Client() - self.assertEquals(client._headers, { - 'authenticate': { - 'username': 'test_user', 'apiKey': 'test_api_key'}}) + self.assertEquals(client.auth.username, 'test_user') + self.assertEquals(client.auth.api_key, 'test_api_key') class ClientMethods(unittest.TestCase): @@ -71,32 +53,6 @@ def test_help(self): help(client) help(client['SERVICE']) - def test_set_raw_header_old(self): - client = SoftLayer.Client( - username='doesnotexist', - api_key='issurelywrong' - ) - client.transport = MagicMock() - client.add_raw_header("RAW", "HEADER") - self.assertEquals(client._raw_headers, {'RAW': 'HEADER'}) - - def test_add_header_invalid(self): - client = SoftLayer.Client( - username='doesnotexist', - api_key='issurelywrong' - ) - client.transport = MagicMock() - self.assertRaises(SoftLayer.SoftLayerError, - client.add_header, "", "HEADER") - - def test_remove_header(self): - client = SoftLayer.Client( - username='doesnotexist', - api_key='issurelywrong' - ) - client.remove_header("authenticate") - self.assertNotIn("authenticate", client._headers) - def test_repr(self): client = SoftLayer.Client( username='doesnotexist', @@ -112,70 +68,6 @@ def test_service_repr(self): self.assertIn("Service", repr(client['SERVICE'])) -class OldAPIClient(unittest.TestCase): - - @patch('SoftLayer.API.make_xml_rpc_api_call') - def test_old_api(self, make_xml_rpc_api_call): - client = SoftLayer.API.Client( - 'SoftLayer_SERVICE', None, 'doesnotexist', 'issurelywrong', - endpoint_url="ENDPOINT") - - client.METHOD() - - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), - headers={ - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}}, - timeout=None, - http_headers={ - 'Content-Type': 'application/xml', - 'User-Agent': USER_AGENT, - }) - - @patch('SoftLayer.API.make_xml_rpc_api_call') - def test_complex_old_api(self, make_xml_rpc_api_call): - client = SoftLayer.API.Client( - 'SoftLayer_SERVICE', None, 'doesnotexist', 'issurelywrong', - endpoint_url="ENDPOINT") - - client.set_result_limit(9, offset=10) - client.set_object_mask({'object': {'attribute': ''}}) - client.add_raw_header("RAW", "HEADER") - - client.METHOD( - 1234, - id=5678, - mask={'object': {'attribute': ''}}, - filter={ - 'TYPE': {'obj': {'attribute': {'operation': '^= prefix'}}}}, - limit=9, offset=10) - - make_xml_rpc_api_call.assert_called_with( - 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (1234, ), - headers={ - 'SoftLayer_SERVICEObjectMask': { - 'mask': {'object': {'attribute': ''}}}, - 'SoftLayer_SERVICEObjectFilter': { - 'TYPE': { - 'obj': {'attribute': {'operation': '^= prefix'}}}}, - 'authenticate': { - 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, - 'SoftLayer_SERVICEInitParameters': {'id': 5678}, - 'resultLimit': {'limit': 9, 'offset': 10}}, - timeout=None, - http_headers={ - 'RAW': 'HEADER', - 'Content-Type': 'application/xml', - 'User-Agent': USER_AGENT, - }) - - def test_old_api_no_service(self): - client = SoftLayer.Client(username='doesnotexist', - api_key='issurelywrong') - self.assertRaises(SoftLayer.SoftLayerError, client.METHOD) - - class APIClient(unittest.TestCase): def setUp(self): self.client = SoftLayer.Client( diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py index 1bc07d381..a4a2742b2 100644 --- a/SoftLayer/tests/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -31,18 +31,18 @@ def get_creds(): class UnauthedUser(unittest.TestCase): def test_failed_auth(self): client = SoftLayer.Client( - 'SoftLayer_User_Customer', None, 'doesnotexist', 'issurelywrong', - timeout=20) - self.assertRaises(SoftLayer.SoftLayerAPIError, - client.getPortalLoginToken) + username='doesnotexist', api_key='issurelywrong', timeout=20) + self.assertRaises( + SoftLayer.SoftLayerAPIError, + client['SoftLayer_User_Customer'].getPortalLoginToken) def test_404(self): client = SoftLayer.Client( - 'SoftLayer_User_Customer', None, 'doesnotexist', 'issurelywrong', - timeout=20, endpoint_url='http://httpbin.org/status/404') + username='doesnotexist', api_key='issurelywrong', timeout=20, + endpoint_url='http://httpbin.org/status/404') try: - client.doSomething() + client['SoftLayer_User_Customer'].doSomething() except SoftLayer.SoftLayerAPIError, e: self.assertEqual(e.faultCode, 404) self.assertIn('NOT FOUND', e.faultString) @@ -93,12 +93,11 @@ def test_dns(self): def test_result_types(self): creds = get_creds() client = SoftLayer.Client( - 'SoftLayer_User_Security_Question', username=creds['username'], api_key=creds['api_key'], endpoint_url=creds['endpoint'], timeout=20) - result = client.getAllObjects() + result = client['SoftLayer_User_Security_Question'].getAllObjects() self.assertIsInstance(result, list) self.assertIsInstance(result[0], dict) self.assertIsInstance(result[0]['viewable'], int) From f5fc8391afd94cb49740fa21360fb66b58e7de90 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Fri, 26 Jul 2013 15:43:21 -0500 Subject: [PATCH 012/168] Version bump --- SoftLayer/consts.py | 2 +- SoftLayer/tests/managers/metadata_tests.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 99b3e25e4..f986ddceb 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -6,7 +6,7 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -VERSION = 'v2.3.0' +VERSION = 'v2.3.1' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3/' diff --git a/SoftLayer/tests/managers/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py index 0004e9b1c..c54f8529d 100644 --- a/SoftLayer/tests/managers/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -92,7 +92,7 @@ def test_basic(self, make_api_call): make_api_call.assert_called_with( 'GET', self.url, timeout=5, - http_headers={'User-Agent': 'SoftLayer Python v2.3.0'}) + http_headers={'User-Agent': 'SoftLayer Python v2.3.1'}) self.assertEqual(make_api_call(), r) @patch('SoftLayer.managers.metadata.make_rest_api_call') diff --git a/docs/conf.py b/docs/conf.py index 01d493dd1..bcaa37508 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,9 +49,9 @@ # built documents. # # The short X.Y version. -version = '2.3.0' +version = '2.3.1' # The full version, including alpha/beta/rc tags. -release = '2.3.0' +release = '2.3.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 3e255ccdc..701c6391d 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ setup( name='SoftLayer', - version='2.3.0', + version='2.3.1', description=description, long_description=long_description, author='SoftLayer Technologies, Inc.', From 3a0729c9be832d63f300aef660574042356bf7d8 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 30 Jul 2013 13:49:31 -0500 Subject: [PATCH 013/168] Adding in initial network functionality. The following commands are supported: sl network summary sl network vlan-list sl network vlan-detail --- SoftLayer/CLI/core.py | 1 + SoftLayer/CLI/modules/network.py | 176 +++++++++++++++++++++++++++++++ SoftLayer/managers/__init__.py | 4 +- SoftLayer/managers/network.py | 80 ++++++++++++++ 4 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 SoftLayer/CLI/modules/network.py create mode 100644 SoftLayer/managers/network.py diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index a9b912817..1559de278 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -13,6 +13,7 @@ firewall Firewall rule and security management hardware View hardware details bmetal Interact with bare metal instances + network Perform various network operations help Show help iscsi View iSCSI details image Manages compute and flex images diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py new file mode 100644 index 000000000..e975c5667 --- /dev/null +++ b/SoftLayer/CLI/modules/network.py @@ -0,0 +1,176 @@ +""" +usage: sl network [] [...] [options] + +Perform various network operations + +The available commands are: + summary Provide a summary view of the network + vlan Manage VLAN options +""" +# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. +# :license: BSD, see LICENSE for more details. + +from os import linesep +import os.path + +from SoftLayer import NetworkManager +from SoftLayer.CLI import ( + CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, + FormattedItem) +from SoftLayer.CLI.helpers import ( + CLIAbort, ArgumentError, SequentialOutput, NestedDict, blank, resolve_id) + + +class NetworkSummary(CLIRunnable): + """ +usage: sl network summary [options] + +Display a network summary + +Options: + --sortby=ARG Column to sort by. options: datacenter, vlans, + subnets, IPs, networking, hardware, ccis, firewall +""" + action = 'summary' + + @staticmethod + def execute(client, args): + mgr = NetworkManager(client) + datacenters = mgr.summary_by_datacenter() + + t = Table([ + 'datacenter', 'vlans', 'subnets', 'IPs', 'networking', + 'hardware', 'ccis' + ]) + t.sortby = args.get('--sortby') or 'datacenter' + + for name, dc in datacenters.iteritems(): + t.add_row([ + name, + dc['vlanCount'], + dc['subnetCount'], + dc['primaryIpCount'], + dc['networkingCount'], + dc['hardwareCount'], + dc['virtualGuestCount'], + ]) + + return t + + +class VlanDetail(CLIRunnable): + """ +usage: sl network vlan-detail [options] + +Get detailed information about objects assigned to a particular VLAN + +Filters: + --no-cci Hide CCI listing + --no-hardware Hide hardware listing +""" + action = 'vlan-detail' + + @staticmethod + def execute(client, args): + mgr = NetworkManager(client) + + vlan = mgr.get_vlan(args.get('')) + + t = Table(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + + t.add_row(['id', vlan['id']]) + t.add_row(['number', vlan['vlanNumber']]) + t.add_row(['datacenter', + vlan['primaryRouter']['datacenter']['longName']]) + t.add_row(['primary router', + vlan['primaryRouter']['fullyQualifiedDomainName']]) + t.add_row(['firewall', 'Yes' if vlan['firewallInterfaces'] else 'No']) + subnets = [] + for subnet in vlan['subnets']: + subnet_table = Table(['Name', 'Value']) + subnet_table.align['Name'] = 'r' + subnet_table.align['Value'] = 'l' + subnet_table.add_row(['id', subnet['id']]) + subnet_table.add_row(['identifier', subnet['networkIdentifier']]) + subnet_table.add_row(['netmask', subnet['netmask']]) + subnet_table.add_row(['gateway', subnet['gateway']]) + subnet_table.add_row(['type', subnet['subnetType']]) + subnet_table.add_row(['usable ips', subnet['usableIpAddressCount']]) + subnets.append(subnet_table) + + t.add_row(['subnets', subnets]) + + if not args.get('--no-cci'): + if vlan['virtualGuests']: + cci_table = Table(['Hostname', 'Domain', 'IP']) + cci_table.align['Hostname'] = 'r' + cci_table.align['IP'] = 'l' + for cci in vlan['virtualGuests']: + cci_table.add_row([cci['hostname'], + cci['domain'], + cci['primaryIpAddress']]) + t.add_row(['ccis', cci_table]) + else: + t.add_row(['cci', 'none']) + + if not args.get('--no-hardware'): + if vlan['hardware']: + hw_table = Table(['Hostname', 'Domain', 'IP']) + hw_table.align['Hostname'] = 'r' + hw_table.align['IP'] = 'l' + for hw in vlan['hardware']: + hw_table.add_row([hw['hostname'], + hw['domain'], + hw['primaryIpAddress']]) + t.add_row(['hardware', hw_table]) + else: + t.add_row(['hardware', 'none']) + + return t + + +class VlanList(CLIRunnable): + """ +usage: sl network vlan-list [options] + +Displays a list of VLANs + +Options: + --sortby=ARG Column to sort by. options: id, number, datacenter, IPs, + hardware, ccis, networking + +Filters: + -d DC, --datacenter=DC datacenter shortname (sng01, dal05, ...) + -n NUM, --number=NUM VLAN number +""" + action = 'vlan-list' + + @staticmethod + def execute(client, args): + mgr = NetworkManager(client) + + t = Table([ + 'id', 'number', 'datacenter', 'IPs', 'hardware', 'ccis', + 'networking', 'firewall' + ]) + t.sortby = args.get('--sortby') or 'id' + + vlans = mgr.list_vlans( + datacenter=args.get('--datacenter'), + vlan_number=args.get('--number') + ) + for vlan in vlans: + t.add_row([ + vlan['id'], + vlan['vlanNumber'], + vlan['primaryRouter']['datacenter']['name'], + vlan['totalPrimaryIpAddressCount'], + len(vlan['hardware']), + len(vlan['virtualGuests']), + len(vlan['networkComponents']), + 'Yes' if vlan['firewallInterfaces'] else 'No', + ]) + + return t diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index 72278d4f5..71aace0e8 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -14,7 +14,9 @@ from SoftLayer.managers.hardware import HardwareManager from SoftLayer.managers.messaging import MessagingManager from SoftLayer.managers.metadata import MetadataManager +from SoftLayer.managers.network import NetworkManager from SoftLayer.managers.ssl import SSLManager __all__ = ['CCIManager', 'DNSManager', 'FirewallManager', 'HardwareManager', - 'MessagingManager', 'MetadataManager', 'SSLManager'] + 'MessagingManager', 'MetadataManager', 'NetworkManager', + 'SSLManager'] diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py new file mode 100644 index 000000000..eff18ec40 --- /dev/null +++ b/SoftLayer/managers/network.py @@ -0,0 +1,80 @@ +""" + SoftLayer.Network + ~~~~~~~~~~~~~ + Network Manager/helpers + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" + +from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin + + +class NetworkManager(IdentifierMixin, object): + """ Manage Networkss """ + def __init__(self, client): + self.client = client + self.account = client['Account'] + self.vlan = client['Network_Vlan'] + + def get_vlan(self, id): + return self.vlan.getObject(id=id, mask=self._get_vlan_mask()) + + def list_vlans(self, datacenter=None, vlan_number=None, **kwargs): + _filter = NestedDict(kwargs.get('filter') or {}) + + if vlan_number: + _filter['networkVlans']['vlanNumber'] = query_filter(vlan_number) + + if datacenter: + _filter['networkVlans']['primaryRouter']['datacenter']['name'] = \ + query_filter(datacenter) + + kwargs['filter'] = _filter.to_dict() + + return self._get_vlans(**kwargs) + + def summary_by_datacenter(self): + datacenters = {} + for vlan in self._get_vlans(): + dc = vlan['primaryRouter']['datacenter'] + name = dc['name'] + if name not in datacenters: + datacenters[name] = { + 'hardwareCount': 0, + 'networkingCount': 0, + 'primaryIpCount': 0, + 'subnetCount': 0, + 'virtualGuestCount': 0, + 'vlanCount': 0, + } + + datacenters[name]['vlanCount'] += 1 + datacenters[name]['hardwareCount'] += len(vlan['hardware']) + datacenters[name]['networkingCount'] += \ + len(vlan['networkComponents']) + datacenters[name]['primaryIpCount'] += \ + vlan['totalPrimaryIpAddressCount'] + datacenters[name]['subnetCount'] += len(vlan['subnets']) + datacenters[name]['virtualGuestCount'] += len(vlan['virtualGuests']) + + return datacenters + + def _get_vlans(self, **kwargs): +# print kwargs + return self.account.getNetworkVlans(mask=self._get_vlan_mask(), **kwargs) + + @staticmethod + def _get_vlan_mask(): + mask = [ + 'firewallInterfaces', + 'hardware', + 'networkComponents', +# 'networkVlanFirewall', + 'primaryRouter[id, fullyQualifiedDomainName, datacenter]', + 'subnets', + 'totalPrimaryIpAddressCount', + 'virtualGuests', + ] + + return 'mask[%s]' % ','.join(mask) \ No newline at end of file From 677935109f20434c9fcfdd21840ffd4a88525186 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 30 Jul 2013 14:45:21 -0500 Subject: [PATCH 014/168] Adding unit tests for new network manager --- SoftLayer/tests/managers/network_tests.py | 91 +++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 SoftLayer/tests/managers/network_tests.py diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py new file mode 100644 index 000000000..6f9668456 --- /dev/null +++ b/SoftLayer/tests/managers/network_tests.py @@ -0,0 +1,91 @@ +""" + SoftLayer.tests.managers.network_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer import NetworkManager + +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +from mock import MagicMock, ANY, call, patch + + +class NetworkTests(unittest.TestCase): + + def setUp(self): + self.client = MagicMock() + self.network = NetworkManager(self.client) + + def test_get_vlan(self): + id = 1234 + mcall = call(id=id, mask=ANY) + service = self.client['Network_Vlan'] + + self.network.get_vlan(id) + service.getObject.assert_has_calls(mcall) + + def test_list_vlans_default(self): + mcall = call(filter={}, mask=ANY) + service = self.client['Account'] + + self.network.list_vlans() + + service.getNetworkVlans.assert_has_calls(mcall) + + def test_list_vlans_with_filters(self): + number = 5 + datacenter = 'dal00' + self.network.list_vlans( + vlan_number=number, + datacenter=datacenter, + ) + + service = self.client['Account'] + service.getNetworkVlans.assert_has_calls(call( + filter={ + 'networkVlans': { + 'primaryRouter': { + 'datacenter': { + 'name': {'operation': '_= ' + datacenter}}, + }, + 'vlanNumber': {'operation': number}, + }, + }, + mask=ANY + )) + + def test_summary_by_datacenter(self): + mcall = call(mask=ANY) + service = self.client['Account'] + + service.getNetworkVlans.return_value = [ + { + 'name': 'dal00', + 'hardware': [], + 'networkComponents': [], + 'primaryRouter': { + 'datacenter': {'name': 'dal00'} + }, + 'totalPrimaryIpAddressCount': 3, + 'subnets': [], + 'virtualGuests': [] + } + ] + + expected = {'dal00': { + 'hardwareCount': 0, + 'networkingCount': 0, + 'primaryIpCount': 3, + 'subnetCount': 0, + 'virtualGuestCount': 0, + 'vlanCount': 1 + }} + + result = self.network.summary_by_datacenter() + + service.getNetworkVlans.assert_has_calls(mcall) + self.assertEqual(expected, result) \ No newline at end of file From ac9242b508868892d564028cd3cfcd143990d24b Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 30 Jul 2013 15:19:38 -0500 Subject: [PATCH 015/168] PEP8 fixes --- SoftLayer/CLI/modules/network.py | 3 ++- SoftLayer/managers/network.py | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index e975c5667..e1886166d 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -97,7 +97,8 @@ def execute(client, args): subnet_table.add_row(['netmask', subnet['netmask']]) subnet_table.add_row(['gateway', subnet['gateway']]) subnet_table.add_row(['type', subnet['subnetType']]) - subnet_table.add_row(['usable ips', subnet['usableIpAddressCount']]) + subnet_table.add_row(['usable ips', + subnet['usableIpAddressCount']]) subnets.append(subnet_table) t.add_row(['subnets', subnets]) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index eff18ec40..ec149ca62 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -25,13 +25,13 @@ def list_vlans(self, datacenter=None, vlan_number=None, **kwargs): if vlan_number: _filter['networkVlans']['vlanNumber'] = query_filter(vlan_number) - + if datacenter: _filter['networkVlans']['primaryRouter']['datacenter']['name'] = \ query_filter(datacenter) kwargs['filter'] = _filter.to_dict() - + return self._get_vlans(**kwargs) def summary_by_datacenter(self): @@ -56,13 +56,14 @@ def summary_by_datacenter(self): datacenters[name]['primaryIpCount'] += \ vlan['totalPrimaryIpAddressCount'] datacenters[name]['subnetCount'] += len(vlan['subnets']) - datacenters[name]['virtualGuestCount'] += len(vlan['virtualGuests']) + datacenters[name]['virtualGuestCount'] += \ + len(vlan['virtualGuests']) return datacenters def _get_vlans(self, **kwargs): -# print kwargs - return self.account.getNetworkVlans(mask=self._get_vlan_mask(), **kwargs) + return self.account.getNetworkVlans(mask=self._get_vlan_mask(), + **kwargs) @staticmethod def _get_vlan_mask(): @@ -70,11 +71,10 @@ def _get_vlan_mask(): 'firewallInterfaces', 'hardware', 'networkComponents', -# 'networkVlanFirewall', 'primaryRouter[id, fullyQualifiedDomainName, datacenter]', 'subnets', 'totalPrimaryIpAddressCount', 'virtualGuests', ] - return 'mask[%s]' % ','.join(mask) \ No newline at end of file + return 'mask[%s]' % ','.join(mask) From 924732dac7fd0550927519d3c6c6b0896781c676 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 30 Jul 2013 15:40:32 -0500 Subject: [PATCH 016/168] PEP8 fixes --- SoftLayer/tests/managers/network_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 6f9668456..22481ae0a 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -84,8 +84,8 @@ def test_summary_by_datacenter(self): 'virtualGuestCount': 0, 'vlanCount': 1 }} - + result = self.network.summary_by_datacenter() service.getNetworkVlans.assert_has_calls(mcall) - self.assertEqual(expected, result) \ No newline at end of file + self.assertEqual(expected, result) From 5998228b44ef66a8044212cce8a5a083e155d783 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 30 Jul 2013 15:44:49 -0500 Subject: [PATCH 017/168] Correcting tilde deficiencies --- SoftLayer/managers/network.py | 2 +- SoftLayer/tests/managers/network_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index ec149ca62..8f073b066 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -1,6 +1,6 @@ """ SoftLayer.Network - ~~~~~~~~~~~~~ + ~~~~~~~~~~~~~~~~~ Network Manager/helpers :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 22481ae0a..14e59731d 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -1,6 +1,6 @@ """ SoftLayer.tests.managers.network_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. From f30d2754cee681bc8b37ab6262077e2aa1a4cdac Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 30 Jul 2013 15:51:48 -0500 Subject: [PATCH 018/168] Adding in comments per peer review notes --- SoftLayer/managers/network.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 8f073b066..600260203 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -11,16 +11,33 @@ class NetworkManager(IdentifierMixin, object): - """ Manage Networkss """ + """ Manage Networks """ def __init__(self, client): self.client = client self.account = client['Account'] self.vlan = client['Network_Vlan'] def get_vlan(self, id): + """ Returns information about a single VLAN. + + :param int id: The unique identifier for the VLAN + + """ return self.vlan.getObject(id=id, mask=self._get_vlan_mask()) def list_vlans(self, datacenter=None, vlan_number=None, **kwargs): + """ Display a list of all VLANs on the account. + + This provides a quick overview of all VLANs including information about + data center residence and the number of devices attached. + + :param string datacenter: If specified, the list will only contain + VLANs in the specified data center. + :param int vlan_number: If specified, the list will only contain the + VLAN matching this VLAN number. + :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) + + """ _filter = NestedDict(kwargs.get('filter') or {}) if vlan_number: @@ -35,6 +52,10 @@ def list_vlans(self, datacenter=None, vlan_number=None, **kwargs): return self._get_vlans(**kwargs) def summary_by_datacenter(self): + """ Provides a dictionary with a summary of all network information on + the account, grouped by data center. + + """ datacenters = {} for vlan in self._get_vlans(): dc = vlan['primaryRouter']['datacenter'] @@ -62,11 +83,23 @@ def summary_by_datacenter(self): return datacenters def _get_vlans(self, **kwargs): + """ Returns a list of VLANs. + + Wrapper method for preventing duplicated code. + + :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) + + """ return self.account.getNetworkVlans(mask=self._get_vlan_mask(), **kwargs) @staticmethod def _get_vlan_mask(): + """ Returns the standard VLAN object mask. + + Wrapper method for preventing duplicated code. + + """ mask = [ 'firewallInterfaces', 'hardware', From 60e22612bfba1ea5011717b47966fd1c5737028c Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 30 Jul 2013 16:18:24 -0500 Subject: [PATCH 019/168] Adding in documentation file --- docs/api/managers/network.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/api/managers/network.rst diff --git a/docs/api/managers/network.rst b/docs/api/managers/network.rst new file mode 100644 index 000000000..3a74efaaa --- /dev/null +++ b/docs/api/managers/network.rst @@ -0,0 +1,6 @@ +.. _network: + +.. automodule:: SoftLayer.managers.network + :members: + :inherited-members: + :undoc-members: From aa6c5931de81b6fab0093b1965e8a6be81bc1663 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 31 Jul 2013 11:50:35 -0500 Subject: [PATCH 020/168] Consistency Updates/Puts CLI more on par with API in docs * Moves auth handlers to its own module * Made CLI and API landing pages in the docs. The primary index links to those pages and little else. * Links to both the CLI and API landing pages in the README.md * Fixes several spacing inconsistencies in the docopt sections --- README.md | 7 ++-- SoftLayer/API.py | 51 +++------------------------- SoftLayer/CLI/modules/bmetal.py | 2 +- SoftLayer/CLI/modules/dns.py | 12 ++++--- SoftLayer/CLI/modules/hardware.py | 24 +++++++------- SoftLayer/__init__.py | 3 +- SoftLayer/auth.py | 49 +++++++++++++++++++++++++++ docs/api/client.rst | 55 ++++++++++++++++++++++--------- docs/api/managers.rst | 19 ----------- docs/cli.rst | 9 ++++- docs/index.rst | 54 +++++------------------------- 11 files changed, 137 insertions(+), 148 deletions(-) create mode 100644 SoftLayer/auth.py delete mode 100644 docs/api/managers.rst diff --git a/README.md b/README.md index 6e53d3c5c..e980b20c2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ SoftLayer API Python Client =========================== SoftLayer API bindings for Python. For use with -[SoftLayer's API](http://sldn.softlayer.com/reference/softlayerapi). For more -documentation, [go here](http://softlayer.github.com/softlayer-api-python-client/). +[SoftLayer's API](http://sldn.softlayer.com/reference/softlayerapi). + + * [Module Documentation](http://softlayer.github.com/softlayer-api-python-client) + * [API Documentation](http://softlayer.github.com/softlayer-api-python-client/client.html) + * [CLI Documentation](http://softlayer.github.com/softlayer-api-python-client/cli.html) 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 diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 8e04aa991..2683595e1 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -6,15 +6,14 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -from SoftLayer.consts import API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT, \ - USER_AGENT -from SoftLayer.transport import make_xml_rpc_api_call -from SoftLayer.exceptions import SoftLayerError +from consts import API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT, USER_AGENT +from transport import make_xml_rpc_api_call +from exceptions import SoftLayerError +from auth import BasicAuthentication, TokenAuthentication import os -__all__ = ['Client', 'BasicAuthentication', 'TokenAuthentication', - 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] +__all__ = ['Client', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] API_USERNAME = None API_KEY = None @@ -30,46 +29,6 @@ ]) -class AuthenticationBase(object): - def get_headers(self): - raise NotImplementedError - - -class TokenAuthentication(AuthenticationBase): - def __init__(self, user_id, auth_token): - self.user_id = user_id - self.auth_token = auth_token - - def get_headers(self): - return { - 'authenticate': { - 'complexType': 'PortalLoginToken', - 'userId': self.user_id, - 'authToken': self.auth_token, - } - } - - def __repr__(self): - return "" % (self.user_id, self.auth_token) - - -class BasicAuthentication(AuthenticationBase): - def __init__(self, username, api_key): - self.username = username - self.api_key = api_key - - def get_headers(self): - return { - 'authenticate': { - 'username': self.username, - 'apiKey': self.api_key, - } - } - - def __repr__(self): - return "" % (self.username) - - class Client(object): """ A SoftLayer API client. diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 1c3ea0b0e..8921d0a12 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -473,7 +473,7 @@ class CancelInstance(CLIRunnable): Options: --immediate Cancels the instance immediately (instead of on the billing - anniversary). + anniversary). """ action = 'cancel' diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 61e171837..3d177fb37 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -3,14 +3,16 @@ Manage DNS -The available commands are: - edit Update resource records (bulk/single) +The available zone commands are: create Create zone + delete Delete zone list List zones or a zone's records - remove Remove resource records - add Add resource record print Print zone in BIND format - delete Delete zone + +The available record commands are: + add Add resource record + edit Update resource records (bulk/single) + remove Remove resource records """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 7d2efbb2a..3e810aca3 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -5,15 +5,15 @@ Manage hardware The available commands are: - list List hardware devices - detail Retrieve hardware details - reload Perform an OS reload - cancel Cancel a dedicated server. + list List hardware devices + detail Retrieve hardware details + reload Perform an OS reload + cancel Cancel a dedicated server. cancel-reasons Provides the list of possible cancellation reasons - network Manage network settings + network Manage network settings list-chassis Provide a list of all chassis available for ordering create-options Display a list of creation options for a specific chassis - create Create a new dedicated server + create Create a new dedicated server For several commands, will be asked for. This can be the id, hostname or the ip address for a piece of hardware. @@ -35,9 +35,9 @@ class ListHardware(CLIRunnable): List hardware servers on the acount Examples: - sl hardware list --datacenter=dal05 - sl hardware list --network=100 --domain=example.com - sl hardware list --tags=production,db + sl hardware list --datacenter=dal05 + sl hardware list --network=100 --domain=example.com + sl hardware list --tags=production,db Options: --sortby=ARG Column to sort by. options: id, datacenter, host, cores, @@ -180,8 +180,8 @@ class HardwareReload(CLIRunnable): 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) + -i, --postinstall=URI Post-install script to download + (Only HTTPS executes, HTTP leaves file in /root) """ action = 'reload' @@ -261,7 +261,7 @@ class NetworkHardware(CLIRunnable): Options: --speed=SPEED Port speed. 0 disables the port. - [Options: 0, 10, 100, 1000, 10000] + [Options: 0, 10, 100, 1000, 10000] --public Public network --private Private network """ diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 2d470535f..cea1d586b 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -18,7 +18,8 @@ from API import * # NOQA from managers import * # NOQA -from SoftLayer.exceptions import * # NOQA +from exceptions import * # NOQA +from auth import * # NOQA __title__ = 'SoftLayer' __version__ = VERSION diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py new file mode 100644 index 000000000..8b6502006 --- /dev/null +++ b/SoftLayer/auth.py @@ -0,0 +1,49 @@ +""" + SoftLayer.auth + ~~~~~~~~~~~~~~ + Module with the supported auth mechanisms for the SoftLayer API + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +__all__ = ['BasicAuthentication', 'TokenAuthentication', 'AuthenticationBase'] + + +class AuthenticationBase(object): + def get_headers(self): + raise NotImplementedError + + +class TokenAuthentication(AuthenticationBase): + def __init__(self, user_id, auth_token): + self.user_id = user_id + self.auth_token = auth_token + + def get_headers(self): + return { + 'authenticate': { + 'complexType': 'PortalLoginToken', + 'userId': self.user_id, + 'authToken': self.auth_token, + } + } + + def __repr__(self): + return "" % (self.user_id, self.auth_token) + + +class BasicAuthentication(AuthenticationBase): + def __init__(self, username, api_key): + self.username = username + self.api_key = api_key + + def get_headers(self): + return { + 'authenticate': { + 'username': self.username, + 'apiKey': self.api_key, + } + } + + def __repr__(self): + return "" % (self.username) diff --git a/docs/api/client.rst b/docs/api/client.rst index dfdf115cd..316dd545f 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -1,9 +1,23 @@ .. _client: -Developer Interface -=================== -This is the primary API client to make API calls. It deals with constructing and executing XML-RPC calls against the SoftLayer API. +API Documentation +================= +This is the primary API client to make API calls. It deals with constructing and executing XML-RPC calls against the SoftLayer API. Below are some links that will help to use the SoftLayer API. + +.. toctree:: + + SoftLayer API Documentation + Source on Github + +:: + + >>> import SoftLayer + >>> client = SoftLayer.Client(username="username", api_key="api_key") + >>> resp = client['Account'].getObject() + >>> resp['companyName'] + 'Your Company' + Getting Started --------------- @@ -121,33 +135,44 @@ API Reference :undoc-members: +Managers +-------- +:: + + >>> from SoftLayer import CCIManager, Client + >>> client = Client(...) + >>> cci = CCIManager(client) + >>> cci.list_instances() + [...] + +Managers mask out a lot of the complexities of using the API into classes that provide a simpler interface to various services. These are higher-level interfaces to the SoftLayer API. + +.. toctree:: + :maxdepth: 2 + :glob: + + managers/* + + Backwards Compatibility ----------------------- -If you've been using the older Python client (<2.0), you'll be happy to know that the old API is still currently working. However, you should deprecate use of the old stuff. Below is an example of the old API converted to the new one. - -.. automodule:: SoftLayer.deprecated - :members: - :undoc-members: +As of 3.0, the old API methods and parameters no longer work. Below are examples of converting the old API to the new one. +**Get the IP address for an account** :: + # Old import SoftLayer.API client = SoftLayer.API.Client('SoftLayer_Account', None, 'username', 'api_key') client.set_object_mask({'ipAddresses' : None}) client.set_result_limit(10, offset=10) client.getObject() -... changes to ... -:: - + # New import SoftLayer client = SoftLayer.Client(username='username', api_key='api_key') client['Account'].getObject(mask="mask[ipAddresses]", limit=10, offset=0) -Deprecated APIs -^^^^^^^^^^^^^^^ -Below are examples of how the old usages to the new API. - **Importing the module** :: diff --git a/docs/api/managers.rst b/docs/api/managers.rst deleted file mode 100644 index c2bf1fe83..000000000 --- a/docs/api/managers.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _managers: - -Managers --------- -:: - - >>> from SoftLayer import CCIManager, Client - >>> client = Client(...) - >>> cci = CCIManager(client) - >>> cci.list_instances() - [...] - -Managers mask out a lot of the complexities of using the API into classes that provide a simpler interface to various services. These are higher-level interfaces to the SoftLayer API. - -.. toctree:: - :maxdepth: 1 - :glob: - - managers/* \ No newline at end of file diff --git a/docs/cli.rst b/docs/cli.rst index 37eca772f..e802708c3 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -3,7 +3,13 @@ Command-line Interface ====================== -The SoftLayer command line interface is available via the `sl` command available in your `PATH`. The `sl` command is a reference implementation of SoftLayer API bindings for python and how to efficiently make API calls. +The SoftLayer command line interface is available via the `sl` command available in your `PATH`. The `sl` command is a reference implementation of SoftLayer API bindings for python and how to efficiently make API calls. See the :ref:`usage-examples` section to see how to discover all of the functionality not fully documented here. + +.. toctree:: + :maxdepth: 2 + + cli/cci + cli/dev Configuration Setup @@ -55,6 +61,7 @@ The only required fields are `username` and `api_key`. You can optionally also/e [softlayer] endpoint_url = https://api.softlayer.com/xmlrpc/v3/ +.. _usage-examples: Usage Examples -------------- diff --git a/docs/index.rst b/docs/index.rst index 21f2fcd28..342cb6eb3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,55 +5,17 @@ SoftLayer API Python Client |version| This is the documentation to SoftLayer's Python API Bindings. These bindings use SoftLayer's `XML-RPC interface `_ in order to manage SoftLayer services. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + :glob: install - SoftLayer API Documentation - Source on Github - SoftLayer Developer Network - Twitter - - -API Documentation ------------------ -:: - - >>> import SoftLayer - >>> client = SoftLayer.Client(username="username", api_key="api_key") - >>> resp = client['Account'].getObject() - >>> resp['companyName'] - 'Your Company' - -.. toctree:: - :maxdepth: 2 - api/client - api/managers - - -Command-Line Interface ----------------------- -:: - - $ sl cci list - :.........:............:....................:.......:........:................:..............:....................: - : id : datacenter : host : cores : memory : primary_ip : backend_ip : active_transaction : - :.........:............:....................:.......:........:................:..............:....................: - : 1234567 : dal05 : test.example.com : 4 : 4G : 12.34.56 : 65.43.21 : - : - :.........:............:....................:.......:........:................:..............:....................: - -.. toctree:: - :maxdepth: 2 - cli - cli/cci - cli/dev - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +External Links +-------------- +.. toctree:: + SoftLayer API Documentation + Source on Github + Twitter From 2923ac3630794fc0121290531fdb0d39b6aa6fb6 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 31 Jul 2013 16:00:27 -0500 Subject: [PATCH 021/168] Moves auth tests to its own file --- SoftLayer/tests/api_tests.py | 52 --------------------------- SoftLayer/tests/auth_tests.py | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 52 deletions(-) create mode 100644 SoftLayer/tests/auth_tests.py diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 7b1fb66b8..950849988 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -261,55 +261,3 @@ def test_authenticate_with_password(self, _call): self.assertIsNotNone(self.client.auth) self.assertEquals(self.client.auth.user_id, 12345) self.assertEquals(self.client.auth.auth_token, 'TOKEN') - - -class TestAuthenticationBase(unittest.TestCase): - def test_get_headers(self): - auth = SoftLayer.API.AuthenticationBase() - self.assertRaises(NotImplementedError, auth.get_headers) - - -class TestBasicAuthentication(unittest.TestCase): - def setUp(self): - self.auth = SoftLayer.BasicAuthentication('USERNAME', 'APIKEY') - - def test_attribs(self): - self.assertEquals(self.auth.username, 'USERNAME') - self.assertEquals(self.auth.api_key, 'APIKEY') - - def test_get_headers(self): - self.assertEquals(self.auth.get_headers(), { - 'authenticate': { - 'username': 'USERNAME', - 'apiKey': 'APIKEY', - } - }) - - def test_repr(self): - s = repr(self.auth) - self.assertIn('BasicAuthentication', s) - self.assertIn('USERNAME', s) - - -class TestTokenAuthentication(unittest.TestCase): - def setUp(self): - self.auth = SoftLayer.TokenAuthentication(12345, 'TOKEN') - - def test_attribs(self): - self.assertEquals(self.auth.user_id, 12345) - self.assertEquals(self.auth.auth_token, 'TOKEN') - - def test_get_headers(self): - self.assertEquals(self.auth.get_headers(), { - 'authenticate': { - 'complexType': 'PortalLoginToken', - 'userId': 12345, - 'authToken': 'TOKEN', - } - }) - - def test_repr(self): - s = repr(self.auth) - self.assertIn('TokenAuthentication', s) - self.assertIn('12345', s) - self.assertIn('TOKEN', s) diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py new file mode 100644 index 000000000..6c0f0bf48 --- /dev/null +++ b/SoftLayer/tests/auth_tests.py @@ -0,0 +1,66 @@ +""" + SoftLayer.tests.auth_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA + +from SoftLayer.auth import ( + AuthenticationBase, BasicAuthentication, TokenAuthentication) + + +class TestAuthenticationBase(unittest.TestCase): + def test_get_headers(self): + auth = AuthenticationBase() + self.assertRaises(NotImplementedError, auth.get_headers) + + +class TestBasicAuthentication(unittest.TestCase): + def setUp(self): + self.auth = BasicAuthentication('USERNAME', 'APIKEY') + + def test_attribs(self): + self.assertEquals(self.auth.username, 'USERNAME') + self.assertEquals(self.auth.api_key, 'APIKEY') + + def test_get_headers(self): + self.assertEquals(self.auth.get_headers(), { + 'authenticate': { + 'username': 'USERNAME', + 'apiKey': 'APIKEY', + } + }) + + def test_repr(self): + s = repr(self.auth) + self.assertIn('BasicAuthentication', s) + self.assertIn('USERNAME', s) + + +class TestTokenAuthentication(unittest.TestCase): + def setUp(self): + self.auth = TokenAuthentication(12345, 'TOKEN') + + def test_attribs(self): + self.assertEquals(self.auth.user_id, 12345) + self.assertEquals(self.auth.auth_token, 'TOKEN') + + def test_get_headers(self): + self.assertEquals(self.auth.get_headers(), { + 'authenticate': { + 'complexType': 'PortalLoginToken', + 'userId': 12345, + 'authToken': 'TOKEN', + } + }) + + def test_repr(self): + s = repr(self.auth) + self.assertIn('TokenAuthentication', s) + self.assertIn('12345', s) + self.assertIn('TOKEN', s) From 7fe75725d01da424dd700f1ef418460c318766e1 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 18 Jul 2013 13:48:39 -0500 Subject: [PATCH 022/168] Changed the command_name to have a default of none and then checked to see if command is specified before using it. This will allow us to have modules without commands. This is useful for modules that only have one command or purpose. --- SoftLayer/CLI/core.py | 4 +++- SoftLayer/tests/CLI/core_tests.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 1559de278..e941dd9bd 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -123,7 +123,9 @@ def parse(self, args): # handle `sl ...` module, module_args = self.parse_module_args( module_name, main_args['']) - command_name = module_args[''] + + # get the command argument + command_name = module_args.get('') # handle `sl ...` return self.parse_command_args( diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 9a2fb81e8..4a003e859 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -24,6 +24,13 @@ def module_fixture(): """ +def module_no_command_fixture(): + """ +usage: sl cci [...] [options] + sl cci [-h | --help] +""" + + class submodule_fixture(object): """ usage: sl cci list [options] @@ -88,6 +95,17 @@ def test_invalid_module(self): SystemExit, cli.core.main, args=['nope', 'list', '--config=path/to/config'], env=self.env) + def test_module_with_no_command(self): + self.env.plugins = { + 'cci': {'list': submodule_fixture, None: submodule_fixture} + } + self.env.get_module_name.return_value = 'cci' + self.env.load_module = MagicMock() + self.env.load_module.return_value = module_no_command_fixture + resolver = cli.core.CommandParser(self.env) + command, command_args = resolver.parse(['cci', 'list']) + self.assertEqual(submodule_fixture, command) + def test_help(self): self.env.get_module_name.return_value = 'help' self.assertRaises( From 4aef716a60cc0a0ba60ddf31ae29ccfc2393b5f7 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 1 Aug 2013 19:42:40 -0500 Subject: [PATCH 023/168] Adds VLAN to cci/hardware details Note: Also fixes status not being masked in for CCIs --- SoftLayer/CLI/modules/cci.py | 18 +++++++++++++----- SoftLayer/CLI/modules/hardware.py | 7 +++++++ SoftLayer/managers/cci.py | 2 ++ SoftLayer/managers/hardware.py | 1 + 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 5b69cdf9e..a61dd7d1c 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -137,14 +137,12 @@ def execute(client, args): t.add_row(['id', result['id']]) t.add_row(['hostname', result['fullyQualifiedDomainName']]) t.add_row(['status', FormattedItem( - result['status']['keyName'], result['status']['name'])]) + result['status']['keyName'] or blank(), + result['status']['name'] or blank() + )]) t.add_row(['state', FormattedItem( result['powerState']['keyName'], result['powerState']['name'])]) t.add_row(['datacenter', result['datacenter']['name']]) - t.add_row(['cores', result['maxCpu']]) - t.add_row(['memory', mb_to_gb(result['maxMemory'])]) - t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) - t.add_row(['private_ip', result['primaryBackendIpAddress'] or blank()]) t.add_row([ 'os', FormattedItem( @@ -153,11 +151,21 @@ def execute(client, args): result['operatingSystem']['softwareLicense'] ['softwareDescription']['name'] or blank() )]) + t.add_row(['cores', result['maxCpu']]) + t.add_row(['memory', mb_to_gb(result['maxMemory'])]) + t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) + t.add_row(['private_ip', result['primaryBackendIpAddress'] or blank()]) t.add_row(['private_only', result['privateNetworkOnlyFlag']]) t.add_row(['private_cpu', result['dedicatedAccountHostOnlyFlag']]) t.add_row(['created', result['createDate']]) t.add_row(['modified', result['modifyDate']]) + vlan_table = Table(['type', 'number', 'id']) + for vlan in result['networkVlans']: + vlan_table.add_row([ + vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) + t.add_row(['vlans', vlan_table]) + if result.get('notes'): t.add_row(['notes', result['notes']]) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 3e810aca3..5c2b1eea1 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -143,6 +143,13 @@ def execute(client, args): ['softwareDescription']['name'] or blank() )]) t.add_row(['created', result['provisionDate'] or blank()]) + + vlan_table = Table(['type', 'number', 'id']) + for vlan in result['networkVlans']: + vlan_table.add_row([ + vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) + t.add_row(['vlans', vlan_table]) + if result.get('notes'): t.add_row(['notes', result['notes']]) diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 47aaad6b9..c6833523c 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -137,6 +137,7 @@ def get_instance(self, id, **kwargs): 'macAddress, primaryIpAddress, port, primarySubnet]', 'lastKnownPowerState.name', 'powerState', + 'status', 'maxCpu', 'maxMemory', 'datacenter', @@ -149,6 +150,7 @@ def get_instance(self, id, **kwargs): 'operatingSystem.passwords[username,password]', 'billingItem.recurringFee', 'tagReferences[id,tag[name,id]]', + 'networkVlans[id,vlanNumber,networkSpace]', ]) kwargs['mask'] = "mask[%s]" % ','.join(items) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index c0544eb35..1eeba00f2 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -230,6 +230,7 @@ def get_hardware(self, id, **kwargs): 'operatingSystem.passwords[username,password]', 'billingItem.recurringFee', 'tagReferences[id,tag[name,id]]', + 'networkVlans[id,vlanNumber,networkSpace]', ]) kwargs['mask'] = "mask[%s]" % ','.join(items) From 2f80417d545fb08b4fd3bbf1995ac547ab4ec30b Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Thu, 1 Aug 2013 11:02:15 -0500 Subject: [PATCH 024/168] Adding some unit tests for the CLI modules to assist in refactoring efforts --- SoftLayer/CLI/modules/hardware.py | 2 +- SoftLayer/tests/CLI/helper_tests.py | 12 - SoftLayer/tests/CLI/modules/__init__.py | 0 SoftLayer/tests/CLI/modules/hardware_tests.py | 231 ++++++++++++++++++ 4 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 SoftLayer/tests/CLI/modules/__init__.py create mode 100644 SoftLayer/tests/CLI/modules/hardware_tests.py diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 5c2b1eea1..ace7a6ef4 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -353,7 +353,7 @@ class HardwareCreateOptions(CLIRunnable): def execute(cls, client, args): mgr = HardwareManager(client) - t = KeyValueTable(['Name', 'Value']) + t = Table(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index da8f808c3..29d1ddf08 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -193,18 +193,6 @@ def test_init(self): self.assertIsInstance(e, cli.helpers.CLIHalt) -class CLIRunnableTypeTests(unittest.TestCase): - - def test_runnable_type(self): - cli.environment.CLIRunnableType.env = cli.environment.Environment() - - class TestCommand(cli.CLIRunnable): - action = 'test' - self.assertEqual( - cli.environment.CLIRunnableType.env.plugins, - {'helper_tests': {'test': TestCommand}}) - - class ResolveIdTests(unittest.TestCase): def test_resolve_id_one(self): diff --git a/SoftLayer/tests/CLI/modules/__init__.py b/SoftLayer/tests/CLI/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py new file mode 100644 index 000000000..8184f1a93 --- /dev/null +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -0,0 +1,231 @@ +""" + SoftLayer.tests.CLI.modules.hardware_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +from mock import MagicMock, patch, call + +from SoftLayer.CLI.modules.hardware import * + + +class HardwareCLITests(unittest.TestCase): + def setUp(self): + self.client = MagicMock() + + @patch('SoftLayer.HardwareManager.get_cancellation_reasons') + @patch('SoftLayer.CLI.helpers.Table.add_row') + def test_HardwareCancelReasons(self, t, reasons): + test_data = { + 'code1': 'Reason 1', + 'code2': 'Reason 2' + } + reasons.return_value = test_data + + HardwareCancelReasons.execute(self.client, {}) + expected = [] + for code, reason in test_data.iteritems(): + expected.append(call([code, reason])) + + self.assertEqual(expected, t.call_args_list) + + @patch('SoftLayer.HardwareManager.get_dedicated_server_create_options') + @patch('SoftLayer.CLI.modules.hardware.Table') + def test_HardwareCreateOptions(self, table, create_options): + args = { + '': 999, + '--all': True, + '--datacenter': False, + '--cpu': False, + '--nic': False, + '--disk': False, + '--os': False, + '--memory': False, + '--controller': False, + } + + # This test data represents the structure of the information returned + # by HardwareManager.get_dedicated_server_create_options. + test_data = self.get_create_options_data() + + create_options.return_value = test_data + + table.mock_add_spec(['align', 'add_row'], True) + + HardwareCreateOptions.execute(self.client, args) + + table_expected = [ + call().add_row(['datacenter', ['FIRST_AVAILABLE', 'TEST00']]), + call(['id', 'description']), + call().add_row([1, 'CPU Core']), + call().add_row(['memory', [2, 4]]), + call().add_row(['os (CLOUDLINUX)', ['CLOUDLINUX_5_32']]), + call().add_row(['os (UBUNTU)', ['UBUNTU_10_32']]), + call().add_row(['os (WIN)', ['WIN_2012-DC-HYPERV_64']]), + call().add_row(['disk', ['100_SATA']]), + call().add_row(['single nic', [100]]), + call().add_row(['dual nic', ['100_DUAL']]), + call().add_row(['disk_controllers', ['RAID5']]), + ] + + table.assert_has_calls(table_expected, any_order=True) + + @staticmethod + def get_create_options_data(): + return { + 'locations': [ + { + 'delivery_information': 'Delivery within 2-4 hours', + 'keyname': 'TEST00', + 'long_name': 'Test Data Center' + }, + { + 'delivery_information': '', + 'keyname': 'FIRST_AVAILABLE', + 'long_name': 'First Available' + } + ], + 'categories': { + 'server': { + 'sort': 0, + 'step': 0, + 'is_required': 1, + 'name': 'Server', + 'group': 'Key Components', + 'items': [ + { + 'id': 1, + 'description': 'CPU Core', + 'sort': 0, + 'price_id': 1, + 'recurring_fee': 0.0, + 'capacity': 0.0, + } + ], + }, + 'ram': { + 'sort': 1, + 'step': 0, + 'is_required': 1, + 'name': 'Memory', + 'group': 'Key Components', + 'items': [ + { + 'id': 21, + 'description': '2GB', + 'sort': 0, + 'price_id': 21, + 'recurring_fee': 0.0, + 'capacity': 2, + }, + { + 'id': 22, + 'description': '4GB', + 'sort': 1, + 'price_id': 22, + 'recurring_fee': 0.0, + 'capacity': 4, + } + ], + }, + 'os': { + 'sort': 2, + 'step': 0, + 'is_required': 1, + 'name': 'Operating Systems', + 'group': 'Key Components', + 'items': [ + { + 'id': 31, + 'description': 'CloudLinux 5 (32 bit)', + 'sort': 0, + 'price_id': 31, + 'recurring_fee': 0.0, + 'capacity': 0.0, + }, + { + 'id': 32, + 'description': 'Windows Server 2012 Datacenter ' + + 'Edition With Hyper-V (64bit)', + 'sort': 0, + 'price_id': 32, + 'recurring_fee': 0.0, + 'capacity': 0.0, + }, + { + 'id': 33, + 'description': 'Ubuntu Linux 10.04 LTS Lucid ' + + 'Lynx (32 bit)', + 'sort': 0, + 'price_id': 33, + 'recurring_fee': 0.0, + 'capacity': 0.0, + } + ], + }, + 'disk0': { + 'sort': 3, + 'step': 0, + 'is_required': 1, + 'name': 'Disk', + 'group': 'Key Components', + 'items': [ + { + 'id': 4, + 'description': '100GB SATA', + 'sort': 0, + 'price_id': 4, + 'recurring_fee': 0.0, + 'capacity': 100.0, + } + ], + }, + 'port_speed': { + 'sort': 4, + 'step': 0, + 'is_required': 1, + 'name': 'NIC', + 'group': 'Key Components', + 'items': [ + { + 'id': 51, + 'description': '100 Mbps', + 'sort': 0, + 'price_id': 51, + 'recurring_fee': 0.0, + 'capacity': 100.0, + }, + { + 'id': 52, + 'description': '100 Mbps dual', + 'sort': 0, + 'price_id': 52, + 'recurring_fee': 0.0, + 'capacity': 100.0, + } + ], + }, + 'disk_controller': { + 'sort': 5, + 'step': 0, + 'is_required': 1, + 'name': 'Disk Controller', + 'group': 'Key Components', + 'items': [ + { + 'id': 6, + 'description': 'RAID 5', + 'sort': 0, + 'price_id': 6, + 'recurring_fee': 0.0, + 'capacity': 0.0, + } + ], + } + } + } From 100a5826ee18ec088cc3b81318cf8d9ea5a1d027 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Thu, 1 Aug 2013 11:14:36 -0500 Subject: [PATCH 025/168] Fixing accidental change to a table and updating unit test accordingly --- SoftLayer/CLI/modules/hardware.py | 2 +- SoftLayer/tests/CLI/modules/hardware_tests.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index ace7a6ef4..5c2b1eea1 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -353,7 +353,7 @@ class HardwareCreateOptions(CLIRunnable): def execute(cls, client, args): mgr = HardwareManager(client) - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index 8184f1a93..7bd8aa48b 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -35,8 +35,10 @@ def test_HardwareCancelReasons(self, t, reasons): self.assertEqual(expected, t.call_args_list) @patch('SoftLayer.HardwareManager.get_dedicated_server_create_options') + @patch('SoftLayer.CLI.modules.hardware.KeyValueTable') @patch('SoftLayer.CLI.modules.hardware.Table') - def test_HardwareCreateOptions(self, table, create_options): + def test_HardwareCreateOptions( + self, cpu_table, option_table, create_options): args = { '': 999, '--all': True, @@ -55,14 +57,16 @@ def test_HardwareCreateOptions(self, table, create_options): create_options.return_value = test_data - table.mock_add_spec(['align', 'add_row'], True) + cpu_table.mock_add_spec(['align', 'add_row'], True) + option_table.mock_add_spec(['align', 'add_row'], True) HardwareCreateOptions.execute(self.client, args) - table_expected = [ + cpu_table_expected = [ + call().add_row([1, 'CPU Core']) + ] + option_table_expected = [ call().add_row(['datacenter', ['FIRST_AVAILABLE', 'TEST00']]), - call(['id', 'description']), - call().add_row([1, 'CPU Core']), call().add_row(['memory', [2, 4]]), call().add_row(['os (CLOUDLINUX)', ['CLOUDLINUX_5_32']]), call().add_row(['os (UBUNTU)', ['UBUNTU_10_32']]), @@ -73,7 +77,8 @@ def test_HardwareCreateOptions(self, table, create_options): call().add_row(['disk_controllers', ['RAID5']]), ] - table.assert_has_calls(table_expected, any_order=True) + cpu_table.assert_has_calls(cpu_table_expected, any_order=True) + option_table.assert_has_calls(option_table_expected, any_order=True) @staticmethod def get_create_options_data(): From d23615ec9e87a46c5dec02bbea0d53788982e983 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Thu, 1 Aug 2013 11:24:50 -0500 Subject: [PATCH 026/168] Starting to improve dedicated server ordering. Using a new API call to generate the create options --- SoftLayer/managers/hardware.py | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 1eeba00f2..bc01a3995 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -421,7 +421,7 @@ def _get_ids_from_ip(self, ip): if results: return [result['id'] for result in results] - def _parse_package_data(self, id): + def _parse_package_data(self, package_id): package = self.client['Product_Package'] results = { @@ -431,7 +431,7 @@ def _parse_package_data(self, id): # First pull the list of available locations. We do it with the # getObject() call so that we get access to the delivery time info. - object_data = package.getRegions(id=id) + object_data = package.getRegions(id=package_id) for loc in object_data: details = loc['location']['locationPackageDetails'][0] @@ -444,7 +444,7 @@ def _parse_package_data(self, id): mask = 'mask[itemCategory[group]]' - for config in package.getConfiguration(id=id, mask=mask): + for config in package.getConfiguration(id=package_id, mask=mask): code = config['itemCategory']['categoryCode'] group = NestedDict(config['itemCategory']) or {} category = { @@ -459,22 +459,22 @@ def _parse_package_data(self, id): results['categories'][code] = category # Now pull in the available package item - for item in package.getItems(id=id, mask='mask[itemCategory]'): - category_code = item['itemCategory']['categoryCode'] - - if category_code not in results['categories']: - results['categories'][category_code] = {'name': category_code, - 'items': []} - results['categories'][category_code]['items'].append({ - 'id': item['id'], - 'description': item['description'], - 'prices': item['prices'], - 'sort': item['prices'][0]['sort'], - 'price_id': item['prices'][0]['id'], - 'recurring_fee': float(item['prices'][0].get('recurringFee', - 0)), - 'capacity': float(item.get('capacity', 0)), - }) + for category in package.getCategories(id=package_id): + code = category['categoryCode'] + items = [] + + for group in category['groups']: + for price in group['prices']: + items.append({ + 'id': price['itemId'], + 'description': price['item']['description'], + 'sort': price['sort'], + 'price_id': price['id'], + 'recurring_fee': price['recurringFee'], + 'capacity': float(price['item'].get('capacity', 0)), + }) + + results['categories'][code]['items'] = items return results From 34540bb80af6c99512e6cf00d87f2316245fc03f Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Thu, 1 Aug 2013 14:04:11 -0500 Subject: [PATCH 027/168] Consolidating mock objects into a function for reuse --- SoftLayer/tests/managers/hardware_tests.py | 177 +++++++-------------- 1 file changed, 60 insertions(+), 117 deletions(-) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index da6d285aa..40ff9cd8a 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -104,40 +104,8 @@ def test_get_bare_metal_create_options_returns_none_on_error(self): def test_get_bare_metal_create_options(self): package_id = 50 - self.client['Product_Package'].getAllObjects.return_value = [ - {'name': 'Bare Metal Instance', 'id': package_id}] - - self.client['Product_Package'].getRegions.return_value = [{ - 'location': { - 'locationPackageDetails': [{ - 'deliveryTimeInformation': 'Typically 2-4 hours', - }], - }, - 'keyname': 'RANDOM_LOCATION', - 'description': 'Random unit testing location', - }] + self._setup_package_mocks(package_id) - self.client['Product_Package'].getConfiguration.return_value = [{ - 'itemCategory': { - 'categoryCode': 'random', - 'name': 'Random Category', - }, - 'sort': 0, - 'orderStepId': 1, - 'isRequired': 0, - }] - - prices = [{'sort': 0, 'id': 999}] - self.client['Product_Package'].getItems.return_value = [{ - 'itemCategory': { - 'categoryCode': 'random2', - 'name': 'Another Category', - }, - 'id': 1000, - 'description': 'Astronaut Sloths', - 'prices': prices, - 'capacity': 0, - }] self.hardware.get_bare_metal_create_options() f1 = self.client['Product_Package'].getRegions @@ -147,63 +115,16 @@ def test_get_bare_metal_create_options(self): f2.assert_called_once_with(id=package_id, mask='mask[itemCategory[group]]') - f3 = self.client['Product_Package'].getItems - f3.assert_called_once_with(id=package_id, - mask='mask[itemCategory]') + f3 = self.client['Product_Package'].getCategories + f3.assert_called_once_with(id=package_id) def test_generate_create_dict_with_all_bare_metal_options(self): package_id = 50 - prices = [{ - 'id': 888, - 'price_id': 1888, - 'sort': 0, - 'setupFee': 0, - 'recurringFee': 0, - 'hourlyRecurringFee': 0, - 'oneTimeFee': 0, - 'laborFee': 0, - }] - - self.client['Product_Package'].getAllObjects.return_value = [ - {'name': 'Bare Metal Instance', 'id': package_id}] - - self.client['Product_Package'].getRegions.return_value = [{ - 'location': { - 'locationPackageDetails': [{ - 'deliveryTimeInformation': 'Typically 2-4 hours', - }], - }, - 'keyname': 'RANDOM_LOCATION', - 'description': 'Random unit testing location', - }] - - self.client['Product_Package'].getConfiguration.return_value = [{ - 'itemCategory': { - 'categoryCode': 'random', - 'name': 'Random Category', - }, - 'sort': 0, - 'orderStepId': 1, - 'isRequired': 1, - 'prices': prices, - }] - - self.client['Product_Package'].getItems.return_value = [{ - 'itemCategory': { - 'categoryCode': 'random', - 'name': 'Random Category', - }, - 'id': 1000, - 'description': 'Astronaut Sloths', - 'prices': prices, - 'capacity': 0, - 'isRequired': 1, - }] + self._setup_package_mocks(package_id) args = { 'server': 100, - 'hourly': False, 'hostname': 'unicorn', 'domain': 'giggles.woo', 'disks': [500], @@ -228,7 +149,6 @@ def test_generate_create_dict_with_all_bare_metal_options(self): {'id': args['disks'][0]}, {'id': args['os']}, {'id': args['port_speed']}, - {'id': prices[0]['id']}, ], } @@ -361,37 +281,8 @@ def test_get_available_dedicated_server_packages(self): def test_get_dedicated_server_options(self): package_id = 13 - self.client['Product_Package'].getRegions.return_value = [{ - 'location': { - 'locationPackageDetails': [{ - 'deliveryTimeInformation': 'Typically 2-4 hours', - }], - }, - 'keyname': 'RANDOM_LOCATION', - 'description': 'Random unit testing location', - }] + self._setup_package_mocks(package_id) - self.client['Product_Package'].getConfiguration.return_value = [{ - 'itemCategory': { - 'categoryCode': 'random', - 'name': 'Random Category', - }, - 'sort': 0, - 'orderStepId': 1, - 'isRequired': 0, - }] - - prices = [{'sort': 0, 'id': 999}] - self.client['Product_Package'].getItems.return_value = [{ - 'itemCategory': { - 'categoryCode': 'random2', - 'name': 'Another Category', - }, - 'id': 1000, - 'description': 'Astronaut Sloths', - 'prices': prices, - 'capacity': 0, - }] self.hardware.get_dedicated_server_create_options(package_id) f1 = self.client['Product_Package'].getRegions @@ -401,9 +292,8 @@ def test_get_dedicated_server_options(self): f2.assert_called_once_with(id=package_id, mask='mask[itemCategory[group]]') - f3 = self.client['Product_Package'].getItems - f3.assert_called_once_with(id=package_id, - mask='mask[itemCategory]') + f3 = self.client['Product_Package'].getCategories + f3.assert_called_once_with(id=package_id) def test_get_default_value_returns_none_for_unknown_category(self): package_options = {'categories': ['Cat1', 'Cat2']} @@ -425,3 +315,56 @@ def test_get_default_value(self): }}} self.assertEqual(price_id, get_default_value(package_options, 'Cat1')) + + def _setup_package_mocks(self, package_id): + self.client['Product_Package'].getAllObjects.return_value = [ + {'name': 'Bare Metal Instance', 'id': package_id}] + + self.client['Product_Package'].getRegions.return_value = [{ + 'location': { + 'locationPackageDetails': [{ + 'deliveryTimeInformation': 'Typically 2-4 hours', + }], + }, + 'keyname': 'RANDOM_LOCATION', + 'description': 'Random unit testing location', + }] + + self.client['Product_Package'].getConfiguration.return_value = [{ + 'itemCategory': { + 'categoryCode': 'random', + 'name': 'Random Category', + }, + 'sort': 0, + 'orderStepId': 1, + 'isRequired': 0, + }] + + prices = [{ + 'itemId': 888, + 'id': 1888, + 'sort': 0, + 'setupFee': 0, + 'recurringFee': 0, + 'hourlyRecurringFee': 0, + 'oneTimeFee': 0, + 'laborFee': 0, + 'item': { + 'id': 888, + 'description': 'Some item', + 'capacity': 0, + } + }] + + self.client['Product_Package'].getCategories.return_value = [{ + 'categoryCode': 'random', + 'name': 'Random Category', + 'id': 1000, + 'groups': [{ + 'sort': 0, + 'prices': prices, + 'itemCategoryId': 1000, + 'packageId': package_id, + }], + }] + \ No newline at end of file From f12acda973f5d7067b8e4326874828968b77815c Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Thu, 1 Aug 2013 15:04:24 -0500 Subject: [PATCH 028/168] Fixing server ordering so that multiple disks are now properly supported --- SoftLayer/CLI/modules/bmetal.py | 10 ++++----- SoftLayer/CLI/modules/hardware.py | 34 +++++++++++++++++++++---------- SoftLayer/managers/hardware.py | 17 ++++++++++------ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 8921d0a12..f26e0c6b7 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -444,11 +444,11 @@ def _get_default_value(cls, bmi_options, option): for item in bmi_options['categories'][option]['items']: if not any([ - float(item['prices'][0].get('setupFee', 0)), - float(item['prices'][0].get('recurringFee', 0)), - float(item['prices'][0].get('hourlyRecurringFee', 0)), - float(item['prices'][0].get('oneTimeFee', 0)), - float(item['prices'][0].get('laborFee', 0)), + 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'] diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 5c2b1eea1..2bdc46626 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -568,7 +568,7 @@ def _generate_windows_code(description): 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'])) + disks.append((disk_type, disk['price_id'], disk['id'])) return [('disk', disks)] elif 'nic' == section: @@ -650,16 +650,15 @@ def execute(cls, client, args): raise CLIAbort('Invalid operating system specified.') order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' - order['server'] = cls._get_price_id_from_options(ds_options, 'cpu', - args['--cpu']) + order['server'] = args['--cpu'] order['ram'] = cls._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 = cls._get_price_id_from_options(ds_options, 'disk', - disk) - + disk_price = cls._get_disk_price(ds_options, disk, disk_number) + disk_number += 1 if disk_price: disk_prices.append(disk_price) @@ -751,16 +750,29 @@ def _get_default_value(cls, ds_options, option): return item['price_id'] @classmethod - def _get_price_id_from_options(cls, ds_options, option, value): + def _get_disk_price(cls, ds_options, value, number): + if not number: + return cls._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 = cls._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'] + + @classmethod + def _get_price_id_from_options(cls, ds_options, option, value, + item_id=False): ds_obj = HardwareCreateOptions() - price_id = None for k, v in ds_obj.get_create_options(ds_options, option, False): for item_options in v: if item_options[0] == value: - price_id = item_options[1] - - return price_id + if not item_id: + return item_options[1] + return item_options[2] class EditHardware(CLIRunnable): diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index bc01a3995..3fa754275 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -470,7 +470,12 @@ def _parse_package_data(self, package_id): 'description': price['item']['description'], 'sort': price['sort'], 'price_id': price['id'], - 'recurring_fee': price['recurringFee'], + 'recurring_fee': price.get('recurringFee', 0), + 'setup_fee': price.get('setupFee', 0), + 'hourly_recurring_fee': price.get('hourlyRecurringFee', + 0), + 'one_time_fee': price.get('oneTimeFee', 0), + 'labor_fee': price.get('laborFee', 0), 'capacity': float(price['item'].get('capacity', 0)), }) @@ -518,10 +523,10 @@ def get_default_value(package_options, category): for item in package_options['categories'][category]['items']: if not any([ - float(item['prices'][0].get('setupFee', 0)), - float(item['prices'][0].get('recurringFee', 0)), - float(item['prices'][0].get('hourlyRecurringFee', 0)), - float(item['prices'][0].get('oneTimeFee', 0)), - float(item['prices'][0].get('laborFee', 0)), + 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'] From 94bf2b8bb522761c981c3fb8072e16c53501ad68 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Thu, 1 Aug 2013 15:52:48 -0500 Subject: [PATCH 029/168] Adding unit test for ListHardware --- SoftLayer/tests/CLI/modules/hardware_tests.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index 7bd8aa48b..043633ae4 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -32,7 +32,7 @@ def test_HardwareCancelReasons(self, t, reasons): for code, reason in test_data.iteritems(): expected.append(call([code, reason])) - self.assertEqual(expected, t.call_args_list) + t.assert_has_calls(expected) @patch('SoftLayer.HardwareManager.get_dedicated_server_create_options') @patch('SoftLayer.CLI.modules.hardware.KeyValueTable') @@ -80,6 +80,51 @@ def test_HardwareCreateOptions( cpu_table.assert_has_calls(cpu_table_expected, any_order=True) option_table.assert_has_calls(option_table_expected, any_order=True) + @patch('SoftLayer.HardwareManager.list_hardware') + @patch('SoftLayer.CLI.helpers.Table.add_row') + @patch('SoftLayer.CLI.modules.hardware.gb') + def test_ListHardware(self, gb, t, list_hardware): + hw_data = [ + { + 'id': 1, + 'datacenter': {'name': 'TEST00', + 'description': 'Test Data Center'}, + 'fullyQualifiedDomainName': 'test1.sftlyr.ws', + 'processorCoreAmount': 2, + 'memoryCapacity': 2, + 'primaryIpAddress': '10.0.0.2', + 'primaryBackendIpAddress': '10.1.0.2', + }, + { + 'id': 2, + 'datacenter': {'name': 'TEST00', + 'description': 'Test Data Center'}, + 'fullyQualifiedDomainName': 'test2.sftlyr.ws', + 'processorCoreAmount': 4, + 'memoryCapacity': 4, + 'primaryIpAddress': '10.0.0.3', + 'primaryBackendIpAddress': '10.1.0.3', + } + ] + list_hardware.return_value = hw_data + gb.side_effect = lambda x: x * 1024 + + ListHardware.execute(self.client, {}) + expected = [] + for server in hw_data: + expected.append(call([ + server['id'], + server['datacenter']['name'], + server['fullyQualifiedDomainName'], + server['processorCoreAmount'], + server['memoryCapacity'] * 1024, + server['primaryIpAddress'], + server['primaryBackendIpAddress'], + ])) + + t.assert_has_calls(expected) + self.assertTrue(gb.called) + @staticmethod def get_create_options_data(): return { From c7505a6e8b96970d073dae7b8ae16e47cfbf0acf Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Thu, 1 Aug 2013 16:19:13 -0500 Subject: [PATCH 030/168] Adding in initial unit test for HardwareDetail --- SoftLayer/tests/CLI/modules/hardware_tests.py | 104 ++++++++++++++---- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index 043633ae4..c3700e3eb 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -80,32 +80,50 @@ def test_HardwareCreateOptions( cpu_table.assert_has_calls(cpu_table_expected, any_order=True) option_table.assert_has_calls(option_table_expected, any_order=True) + @patch('SoftLayer.HardwareManager.get_hardware') + @patch('SoftLayer.CLI.helpers.Table.add_row') + @patch('SoftLayer.CLI.modules.hardware.FormattedItem') + @patch('SoftLayer.CLI.modules.hardware.resolve_id') + @patch('SoftLayer.CLI.modules.hardware.gb') + def test_HardwareDetails( + self, gb, resolve_id, formatted_item, t, get_hardware): + hw_id = 1234 + + def resolve_mock(resolver, identifier, name='object'): + return hw_id + + def formatted_item_mock(short_name, long_name): + return short_name + + resolve_id.side_effect = resolve_mock + formatted_item.side_effect = formatted_item_mock + gb.side_effect = lambda x: x * 1024 + servers = self.get_server_mocks() + get_hardware.return_value = servers[0] + + HardwareDetails.execute(self.client, {'': hw_id}) + + expected = [ + call(['id', 1]), + call(['hostname', 'test1.sftlyr.ws']), + call(['status', 'ACTIVE']), + call(['datacenter', 'TEST00']), + call(['cores', 2]), + call(['memory', 2048]), + call(['public_ip', '10.0.0.2']), + call(['private_ip', '10.1.0.2']), + call(['os', 'Ubuntu']), + call(['created', '2013-08-01 15:23:45']), + call(['notes', 'These are test notes.']) + ] + + t.assert_has_calls(expected) + @patch('SoftLayer.HardwareManager.list_hardware') @patch('SoftLayer.CLI.helpers.Table.add_row') @patch('SoftLayer.CLI.modules.hardware.gb') def test_ListHardware(self, gb, t, list_hardware): - hw_data = [ - { - 'id': 1, - 'datacenter': {'name': 'TEST00', - 'description': 'Test Data Center'}, - 'fullyQualifiedDomainName': 'test1.sftlyr.ws', - 'processorCoreAmount': 2, - 'memoryCapacity': 2, - 'primaryIpAddress': '10.0.0.2', - 'primaryBackendIpAddress': '10.1.0.2', - }, - { - 'id': 2, - 'datacenter': {'name': 'TEST00', - 'description': 'Test Data Center'}, - 'fullyQualifiedDomainName': 'test2.sftlyr.ws', - 'processorCoreAmount': 4, - 'memoryCapacity': 4, - 'primaryIpAddress': '10.0.0.3', - 'primaryBackendIpAddress': '10.1.0.3', - } - ] + hw_data = self.get_server_mocks() list_hardware.return_value = hw_data gb.side_effect = lambda x: x * 1024 @@ -215,7 +233,7 @@ def get_create_options_data(): 'price_id': 33, 'recurring_fee': 0.0, 'capacity': 0.0, - } + } ], }, 'disk0': { @@ -278,4 +296,42 @@ def get_create_options_data(): ], } } - } + } + + @staticmethod + def get_server_mocks(): + return [ + { + 'id': 1, + 'datacenter': {'name': 'TEST00', + 'description': 'Test Data Center'}, + 'fullyQualifiedDomainName': 'test1.sftlyr.ws', + 'processorCoreAmount': 2, + 'memoryCapacity': 2, + 'primaryIpAddress': '10.0.0.2', + 'primaryBackendIpAddress': '10.1.0.2', + 'hardwareStatus': {'status': 'ACTIVE'}, + 'provisionDate': '2013-08-01 15:23:45', + 'notes': 'These are test notes.', + 'operatingSystem': { + 'softwareLicense': { + 'softwareDescription': { + 'referenceCode': 'Ubuntu', + 'name': 'Ubuntu 12.04 LTS', + } + } + } + }, + { + 'id': 2, + 'datacenter': {'name': 'TEST00', + 'description': 'Test Data Center'}, + 'fullyQualifiedDomainName': 'test2.sftlyr.ws', + 'processorCoreAmount': 4, + 'memoryCapacity': 4, + 'primaryIpAddress': '10.0.0.3', + 'primaryBackendIpAddress': '10.1.0.3', + 'hardwareStatus': {'status': 'ACTIVE'}, + 'provisionDate': '2013-08-03 07:15:22', + } + ] From d8f7264102ec8a6ee053b3858e0d41047b7494d8 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 2 Aug 2013 09:12:33 -0500 Subject: [PATCH 031/168] Additional fixes for multiple disk ordering --- SoftLayer/CLI/modules/hardware.py | 3 ++- SoftLayer/managers/hardware.py | 11 +++++++++++ SoftLayer/tests/CLI/modules/hardware_tests.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 2bdc46626..c5a719d7a 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -580,7 +580,8 @@ def _generate_windows_code(description): dual.append((str(int(item['capacity'])) + '_DUAL', item['price_id'])) else: - single.append((int(item['capacity']), item['price_id'])) + single.append((str(int(item['capacity'])), + item['price_id'])) return [('single nic', single), ('dual nic', dual)] elif 'disk_controller' == section: diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 3fa754275..1020b263f 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -343,6 +343,11 @@ def _generate_create_dict( 'bareMetalInstanceFlag': bare_metal, 'hostname': hostname, 'domain': domain, + # TODO - It would be nice if we could get this working too. + # VLAN number doesn't appear to work. + #'networkVlans': [ + # {'vlanNumber': 1836}, {'vlanNumber': 1126} + #], }], 'location': location, 'prices': [ @@ -380,6 +385,12 @@ def _generate_create_dict( required_fields = [] for category, data in p_options['categories'].iteritems(): if data.get('is_required') and category not in arguments: + if 'disk' in category: + # This block makes sure that we can default unspecified + # disks if the user hasn't specified enough. + disk_count = int(category.replace('disk', '')) + if len(disks) >= disk_count + 1: + continue required_fields.append(category) for category in required_fields: diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index c3700e3eb..3118be20a 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -72,7 +72,7 @@ def test_HardwareCreateOptions( call().add_row(['os (UBUNTU)', ['UBUNTU_10_32']]), call().add_row(['os (WIN)', ['WIN_2012-DC-HYPERV_64']]), call().add_row(['disk', ['100_SATA']]), - call().add_row(['single nic', [100]]), + call().add_row(['single nic', ['100']]), call().add_row(['dual nic', ['100_DUAL']]), call().add_row(['disk_controllers', ['RAID5']]), ] From 61707c5800714fcecd6c81cc576c65835846d074 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 2 Aug 2013 10:25:26 -0500 Subject: [PATCH 032/168] Refactored CLI unit tests into integration tests to reduce complexity and improve maintainability. --- SoftLayer/tests/CLI/modules/hardware_tests.py | 181 ++++++++++-------- 1 file changed, 101 insertions(+), 80 deletions(-) diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index 3118be20a..180f60cc3 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -2,6 +2,9 @@ SoftLayer.tests.CLI.modules.hardware_tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + This is a series of integration tests designed to test the complete + command line interface. + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ @@ -11,6 +14,7 @@ import unittest # NOQA from mock import MagicMock, patch, call +from SoftLayer.CLI.helpers import format_output from SoftLayer.CLI.modules.hardware import * @@ -19,26 +23,22 @@ def setUp(self): self.client = MagicMock() @patch('SoftLayer.HardwareManager.get_cancellation_reasons') - @patch('SoftLayer.CLI.helpers.Table.add_row') - def test_HardwareCancelReasons(self, t, reasons): + def test_HardwareCancelReasons(self, reasons): test_data = { 'code1': 'Reason 1', 'code2': 'Reason 2' } reasons.return_value = test_data - HardwareCancelReasons.execute(self.client, {}) - expected = [] - for code, reason in test_data.iteritems(): - expected.append(call([code, reason])) + output = HardwareCancelReasons.execute(self.client, {}) + + expected = [{'Reason': 'Reason 1', 'Code': 'code1'}, + {'Reason': 'Reason 2', 'Code': 'code2'}] - t.assert_has_calls(expected) + self.assertEqual(expected, format_output(output, 'python')) @patch('SoftLayer.HardwareManager.get_dedicated_server_create_options') - @patch('SoftLayer.CLI.modules.hardware.KeyValueTable') - @patch('SoftLayer.CLI.modules.hardware.Table') - def test_HardwareCreateOptions( - self, cpu_table, option_table, create_options): + def test_HardwareCreateOptions(self, create_options): args = { '': 999, '--all': True, @@ -57,91 +57,80 @@ def test_HardwareCreateOptions( create_options.return_value = test_data - cpu_table.mock_add_spec(['align', 'add_row'], True) - option_table.mock_add_spec(['align', 'add_row'], True) + output = HardwareCreateOptions.execute(self.client, args) - HardwareCreateOptions.execute(self.client, args) - - cpu_table_expected = [ - call().add_row([1, 'CPU Core']) - ] - option_table_expected = [ - call().add_row(['datacenter', ['FIRST_AVAILABLE', 'TEST00']]), - call().add_row(['memory', [2, 4]]), - call().add_row(['os (CLOUDLINUX)', ['CLOUDLINUX_5_32']]), - call().add_row(['os (UBUNTU)', ['UBUNTU_10_32']]), - call().add_row(['os (WIN)', ['WIN_2012-DC-HYPERV_64']]), - call().add_row(['disk', ['100_SATA']]), - call().add_row(['single nic', ['100']]), - call().add_row(['dual nic', ['100_DUAL']]), - call().add_row(['disk_controllers', ['RAID5']]), - ] + expected = { + 'datacenter': ['FIRST_AVAILABLE', 'TEST00'], + 'dual nic': ['100_DUAL'], + 'disk_controllers': ['RAID5'], + 'os (CLOUDLINUX)': ['CLOUDLINUX_5_32'], + 'os (WIN)': ['WIN_2012-DC-HYPERV_64'], + 'memory': [2, 4], + 'disk': ['100_SATA'], + 'single nic': ['100'], + 'cpu': [{'id': 1, 'description': 'CPU Core'}], + 'os (UBUNTU)': ['UBUNTU_10_32'] + } - cpu_table.assert_has_calls(cpu_table_expected, any_order=True) - option_table.assert_has_calls(option_table_expected, any_order=True) + self.assertEqual(expected, format_output(output, 'python')) @patch('SoftLayer.HardwareManager.get_hardware') - @patch('SoftLayer.CLI.helpers.Table.add_row') - @patch('SoftLayer.CLI.modules.hardware.FormattedItem') - @patch('SoftLayer.CLI.modules.hardware.resolve_id') - @patch('SoftLayer.CLI.modules.hardware.gb') - def test_HardwareDetails( - self, gb, resolve_id, formatted_item, t, get_hardware): + def test_HardwareDetails(self, get_hardware): hw_id = 1234 - def resolve_mock(resolver, identifier, name='object'): - return hw_id - - def formatted_item_mock(short_name, long_name): - return short_name - - resolve_id.side_effect = resolve_mock - formatted_item.side_effect = formatted_item_mock - gb.side_effect = lambda x: x * 1024 servers = self.get_server_mocks() get_hardware.return_value = servers[0] - HardwareDetails.execute(self.client, {'': hw_id}) + output = HardwareDetails.execute(self.client, {'': hw_id}) - expected = [ - call(['id', 1]), - call(['hostname', 'test1.sftlyr.ws']), - call(['status', 'ACTIVE']), - call(['datacenter', 'TEST00']), - call(['cores', 2]), - call(['memory', 2048]), - call(['public_ip', '10.0.0.2']), - call(['private_ip', '10.1.0.2']), - call(['os', 'Ubuntu']), - call(['created', '2013-08-01 15:23:45']), - call(['notes', 'These are test notes.']) - ] + expected = { + 'status': 'ACTIVE', + 'datacenter': 'TEST00', + 'created': '2013-08-01 15:23:45', + 'notes': 'These are test notes.', + 'hostname': 'test1.sftlyr.ws', + 'public_ip': '10.0.0.2', + 'private_ip': '10.1.0.2', + 'memory': 2048, + 'cores': 2, + 'vlans': [], + 'os': + 'Ubuntu', 'id': 1, + 'vlans': [{'id': 9653, 'number': 1800, 'type': 'PRIVATE'}, + {'id': 19082, 'number': 3672, 'type': 'PUBLIC'}] + } - t.assert_has_calls(expected) + self.assertEqual(expected, format_output(output, 'python')) @patch('SoftLayer.HardwareManager.list_hardware') - @patch('SoftLayer.CLI.helpers.Table.add_row') - @patch('SoftLayer.CLI.modules.hardware.gb') - def test_ListHardware(self, gb, t, list_hardware): + def test_ListHardware(self, list_hardware): hw_data = self.get_server_mocks() list_hardware.return_value = hw_data - gb.side_effect = lambda x: x * 1024 - ListHardware.execute(self.client, {}) - expected = [] - for server in hw_data: - expected.append(call([ - server['id'], - server['datacenter']['name'], - server['fullyQualifiedDomainName'], - server['processorCoreAmount'], - server['memoryCapacity'] * 1024, - server['primaryIpAddress'], - server['primaryBackendIpAddress'], - ])) + output = ListHardware.execute(self.client, {'--tags': 'openstack'}) + + expected = [ + { + 'datacenter': 'TEST00', + 'primary_ip': '10.0.0.2', + 'host': 'test1.sftlyr.ws', + 'memory': 2048, + 'cores': 2, + 'id': 1, + 'backend_ip': '10.1.0.2' + }, + { + 'datacenter': 'TEST00', + 'primary_ip': '10.0.0.3', + 'host': 'test2.sftlyr.ws', + 'memory': 4096, + 'cores': 4, + 'id': 2, + 'backend_ip': '10.1.0.3' + } + ] - t.assert_has_calls(expected) - self.assertTrue(gb.called) + self.assertEqual(expected, format_output(output, 'python')) @staticmethod def get_create_options_data(): @@ -320,7 +309,19 @@ def get_server_mocks(): 'name': 'Ubuntu 12.04 LTS', } } - } + }, + 'networkVlans': [ + { + 'networkSpace': 'PRIVATE', + 'vlanNumber': 1800, + 'id': 9653 + }, + { + 'networkSpace': 'PUBLIC', + 'vlanNumber': 3672, + 'id': 19082 + }, + ] }, { 'id': 2, @@ -333,5 +334,25 @@ def get_server_mocks(): 'primaryBackendIpAddress': '10.1.0.3', 'hardwareStatus': {'status': 'ACTIVE'}, 'provisionDate': '2013-08-03 07:15:22', + 'operatingSystem': { + 'softwareLicense': { + 'softwareDescription': { + 'referenceCode': 'Ubuntu', + 'name': 'Ubuntu 12.04 LTS', + } + } + }, + 'networkVlans': [ + { + 'networkSpace': 'PRIVATE', + 'vlanNumber': 1800, + 'id': 9653 + }, + { + 'networkSpace': 'PUBLIC', + 'vlanNumber': 3672, + 'id': 19082 + }, + ] } ] From 944509ac04eb4401348cffa1741db848f995a97d Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 2 Aug 2013 13:28:06 -0500 Subject: [PATCH 033/168] Bug and PEP8 fixes in the hardware manager. Added integration tests for cancel and reload hardware --- SoftLayer/CLI/modules/hardware.py | 9 ++- SoftLayer/tests/CLI/modules/hardware_tests.py | 78 +++++++++++++++++-- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index c5a719d7a..d816ece59 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -222,10 +222,10 @@ class CancelHardware(CLIRunnable): @classmethod def execute(cls, client, args): hw = HardwareManager(client) - hw_id = resolve_id(hw, args.get('')) + hw_id = resolve_id( + hw.resolve_ids, args.get(''), 'hardware') - cls.env.out("(Optional) Add a cancellation comment:", nl=False) - comment = raw_input() + comment = cls.env.input("(Optional) Add a cancellation comment:") reason = args.get('--reason') @@ -756,7 +756,8 @@ def _get_disk_price(cls, ds_options, value, number): return cls._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 = cls._get_price_id_from_options(ds_options, 'disk', value, True) + item_id = cls._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']: diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index 180f60cc3..b947dd975 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -12,7 +12,7 @@ import unittest2 as unittest except ImportError: import unittest # NOQA -from mock import MagicMock, patch, call +from mock import Mock, MagicMock, patch from SoftLayer.CLI.helpers import format_output from SoftLayer.CLI.modules.hardware import * @@ -81,7 +81,17 @@ def test_HardwareDetails(self, get_hardware): servers = self.get_server_mocks() get_hardware.return_value = servers[0] - output = HardwareDetails.execute(self.client, {'': hw_id}) + dns_mock = Mock() + dns_mock.getReverseDomainRecords = Mock() + dns_mock.getReverseDomainRecords.return_value = [{ + 'resourceRecords': [{'data': '2.0.0.10.in-addr.arpa'}] + }] + client = Mock() + client.__getitem__ = Mock() + client.__getitem__.return_value = dns_mock + + args = {'': hw_id, '--passwords': True} + output = HardwareDetails.execute(client, args) expected = { 'status': 'ACTIVE', @@ -93,9 +103,9 @@ def test_HardwareDetails(self, get_hardware): 'private_ip': '10.1.0.2', 'memory': 2048, 'cores': 2, - 'vlans': [], - 'os': - 'Ubuntu', 'id': 1, + 'ptr': '2.0.0.10.in-addr.arpa', + 'os': 'Ubuntu', 'id': 1, + 'users': ['root abc123'], 'vlans': [{'id': 9653, 'number': 1800, 'type': 'PRIVATE'}, {'id': 19082, 'number': 3672, 'type': 'PUBLIC'}] } @@ -132,6 +142,59 @@ def test_ListHardware(self, list_hardware): self.assertEqual(expected, format_output(output, 'python')) + @patch('SoftLayer.CLI.modules.hardware.CLIAbort') + @patch('SoftLayer.CLI.modules.hardware.no_going_back') + @patch('SoftLayer.HardwareManager.reload') + @patch('SoftLayer.CLI.modules.hardware.resolve_id') + def test_HardwareReload( + self, resolve_mock, reload_mock, ngb_mock, abort_mock): + hw_id = 12345 + resolve_mock.return_value = hw_id + ngb_mock.return_value = False + + # Check the positive case + args = {'--really': True, '--postinstall': None} + HardwareReload.execute(self.client, args) + + reload_mock.assert_called_with(hw_id, args['--postinstall']) + + # Now check to make sure we properly call CLIAbort in the negative case + args['--really'] = False + + HardwareReload.execute(self.client, args) + abort_mock.assert_called() + + @patch('SoftLayer.CLI.modules.hardware.CLIAbort') + @patch('SoftLayer.CLI.modules.hardware.no_going_back') + @patch('SoftLayer.HardwareManager.cancel_hardware') + @patch('SoftLayer.CLI.modules.hardware.resolve_id') + def test_CancelHardware( + self, resolve_mock, cancel_mock, ngb_mock, abort_mock): + hw_id = 12345 + resolve_mock.return_value = hw_id + ngb_mock.return_value = False + + env_mock = Mock() + env_mock.input = Mock() + env_mock.input.return_value = 'Comment' + + CancelHardware.env = env_mock + env_mock.assert_called() + + # Check the positive case + args = {'--really': True, '--reason': 'Test'} + CancelHardware.execute(self.client, args) + + cancel_mock.assert_called_with(hw_id, args['--reason'], 'Comment') + + # Now check to make sure we properly call CLIAbort in the negative case + env_mock.reset_mock() + args['--really'] = False + + CancelHardware.execute(self.client, args) + abort_mock.assert_called() + env_mock.assert_called() + @staticmethod def get_create_options_data(): return { @@ -308,7 +371,10 @@ def get_server_mocks(): 'referenceCode': 'Ubuntu', 'name': 'Ubuntu 12.04 LTS', } - } + }, + 'passwords': [ + {'username': 'root', 'password': 'abc123'} + ], }, 'networkVlans': [ { From 23166ad849883faa5e7a915770fea9bc0b7cc643 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 2 Aug 2013 14:30:51 -0500 Subject: [PATCH 034/168] More integration tests and bug fixes --- SoftLayer/CLI/modules/hardware.py | 14 +- SoftLayer/tests/CLI/modules/hardware_tests.py | 145 +++++++++++++++++- 2 files changed, 148 insertions(+), 11 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index d816ece59..ec9732c5b 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -301,7 +301,7 @@ def exec_port(client, args): @staticmethod def exec_detail(client, args): # TODO this should print out default gateway and stuff - raise CLIAbort('Not implemented') + CLIAbort('Not implemented') class ListChassisHardware(CLIRunnable): @@ -596,8 +596,6 @@ def _generate_windows_code(description): return [('disk_controllers', options)] - return [] - class CreateHardware(CLIRunnable): """ @@ -742,11 +740,11 @@ def _get_default_value(cls, ds_options, option): for item in ds_options['categories'][option]['items']: if not any([ - float(item['prices'][0].get('setupFee', 0)), - float(item['prices'][0].get('recurringFee', 0)), - float(item['prices'][0].get('hourlyRecurringFee', 0)), - float(item['prices'][0].get('oneTimeFee', 0)), - float(item['prices'][0].get('laborFee', 0)), + 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'] diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index b947dd975..5ffbfd5b1 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -63,7 +63,7 @@ def test_HardwareCreateOptions(self, create_options): 'datacenter': ['FIRST_AVAILABLE', 'TEST00'], 'dual nic': ['100_DUAL'], 'disk_controllers': ['RAID5'], - 'os (CLOUDLINUX)': ['CLOUDLINUX_5_32'], + 'os (CLOUDLINUX)': ['CLOUDLINUX_5_32_MINIMAL'], 'os (WIN)': ['WIN_2012-DC-HYPERV_64'], 'memory': [2, 4], 'disk': ['100_SATA'], @@ -195,6 +195,127 @@ def test_CancelHardware( abort_mock.assert_called() env_mock.assert_called() + @patch('SoftLayer.CLI.modules.hardware.CLIAbort') + @patch('SoftLayer.HardwareManager.change_port_speed') + @patch('SoftLayer.CLI.modules.hardware.resolve_id') + def test_NetworkHardware( + self, resolve_mock, port_mock, abort_mock): + hw_id = 12345 + resolve_mock.return_value = hw_id + + # Check the details case first + args = {'port': False, 'details': True} + NetworkHardware.execute(self.client, args) + abort_mock.assert_called() + + # Now test updating the port + args['port'] = True + args['detail'] = False + args['--private'] = True + args['--speed'] = 100 + + port_mock.side_effect = [True, False] + + # First call simulates a success + NetworkHardware.execute(self.client, args) + port_mock.assert_called_with(hw_id, False, 100) + + # Second call simulates an error + self.assertFalse(NetworkHardware.execute(self.client, args)) + + @patch('SoftLayer.HardwareManager.get_available_dedicated_server_packages') + def test_ListChassisHardware(self, packages): + test_data = [ + (1, 'Chassis 1'), + (2, 'Chassis 2') + ] + packages.return_value = test_data + + output = ListChassisHardware.execute(self.client, {}) + + expected = [ + {'Chassis': 'Chassis 1', 'Code': 1}, + {'Chassis': 'Chassis 2', 'Code': 2} + ] + + self.assertEqual(expected, format_output(output, 'python')) + + @patch('SoftLayer.HardwareManager.get_dedicated_server_create_options') + def test_CreateHardware(self, create_options): + args = { + '--chassis': 999, + '--hostname': 'test', + '--domain': 'example.com', + '--datacenter': 'TEST00', + '--cpu': False, + '--network': '100', + '--disk': ['100_SATA', '100_SATA'], + '--os': 'CLOUDLINUX_5_32_MINIMAL', + '--memory': False, + '--controller': False, + '--test': True, + } + + # This test data represents the structure of the information returned + # by HardwareManager.get_dedicated_server_create_options. + test_data = self.get_create_options_data() + + create_options.return_value = test_data + + # First, test the --test flag + with patch('SoftLayer.HardwareManager.verify_order') as verify_mock: + verify_mock.return_value = { + 'prices': [ + { + 'recurringFee': 0.0, + 'setupFee': 0.0, + 'item': {'description': 'First Item'}, + }, + { + 'recurringFee': 25.0, + 'setupFee': 0.0, + 'item': {'description': 'Second Item'}, + } + ] + } + output = CreateHardware.execute(self.client, args) + + # This test is fragile. We need to figure out why format 'python' + # doesn't work here in the CLI code. + expected = """:....................:.......: +: Item : cost : +:....................:.......: +: First Item : 0.00 : +: Second Item : 25.00 : +: Total monthly cost : 25.00 : +:....................:.......: + -- ! Prices reflected here are retail and do not take account level discounts and are not guarenteed.""" + self.assertEqual(expected, format_output(output, 'table')) + + # Now test ordering + with patch('SoftLayer.HardwareManager.place_order') as order_mock: + order_mock.return_value = { + 'orderId': 98765, + 'orderDate': '2013-08-02 15:23:47' + } + + args['--test'] = False + args['--really'] = True + + output = CreateHardware.execute(self.client, args) + + expected = {'id': 98765, 'created': '2013-08-02 15:23:47'} + self.assertEqual(expected, format_output(output, 'python')) + + # Finally, test cancelling the process + with patch('SoftLayer.CLI.modules.hardware.confirm') as confirm: + confirm.return_value = False + + args['--really'] = False + + self.assertRaises(CLIAbort, + CreateHardware.execute, self.client, args) + @staticmethod def get_create_options_data(): return { @@ -262,7 +383,8 @@ def get_create_options_data(): 'items': [ { 'id': 31, - 'description': 'CloudLinux 5 (32 bit)', + 'description': 'CloudLinux 5 - Minimal Install ' + + '(32 bit)', 'sort': 0, 'price_id': 31, 'recurring_fee': 0.0, @@ -270,7 +392,7 @@ def get_create_options_data(): }, { 'id': 32, - 'description': 'Windows Server 2012 Datacenter ' + + 'description': 'Windows Server 2012 Datacenter' + 'Edition With Hyper-V (64bit)', 'sort': 0, 'price_id': 32, @@ -305,6 +427,23 @@ def get_create_options_data(): } ], }, + 'disk1': { + 'sort': 3, + 'step': 0, + 'is_required': 1, + 'name': 'Disk', + 'group': 'Key Components', + 'items': [ + { + 'id': 4, + 'description': '100GB SATA', + 'sort': 0, + 'price_id': 4, + 'recurring_fee': 10.0, + 'capacity': 100.0, + } + ], + }, 'port_speed': { 'sort': 4, 'step': 0, From dbd1f7c1fa55436f823b11531e89614bda8bd177 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 2 Aug 2013 14:54:35 -0500 Subject: [PATCH 035/168] Removing dead code path --- SoftLayer/CLI/modules/hardware.py | 8 -------- SoftLayer/tests/CLI/modules/hardware_tests.py | 19 +++++++------------ 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index ec9732c5b..a801373e6 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -279,9 +279,6 @@ def execute(cls, client, args): if args['port']: return cls.exec_port(client, args) - if args['details']: - return cls.exec_detail(client, args) - @staticmethod def exec_port(client, args): public = True @@ -298,11 +295,6 @@ def exec_port(client, args): else: return result - @staticmethod - def exec_detail(client, args): - # TODO this should print out default gateway and stuff - CLIAbort('Not implemented') - class ListChassisHardware(CLIRunnable): """ diff --git a/SoftLayer/tests/CLI/modules/hardware_tests.py b/SoftLayer/tests/CLI/modules/hardware_tests.py index 5ffbfd5b1..8a53754b7 100644 --- a/SoftLayer/tests/CLI/modules/hardware_tests.py +++ b/SoftLayer/tests/CLI/modules/hardware_tests.py @@ -195,24 +195,19 @@ def test_CancelHardware( abort_mock.assert_called() env_mock.assert_called() - @patch('SoftLayer.CLI.modules.hardware.CLIAbort') @patch('SoftLayer.HardwareManager.change_port_speed') @patch('SoftLayer.CLI.modules.hardware.resolve_id') def test_NetworkHardware( - self, resolve_mock, port_mock, abort_mock): + self, resolve_mock, port_mock): hw_id = 12345 resolve_mock.return_value = hw_id - # Check the details case first - args = {'port': False, 'details': True} - NetworkHardware.execute(self.client, args) - abort_mock.assert_called() - - # Now test updating the port - args['port'] = True - args['detail'] = False - args['--private'] = True - args['--speed'] = 100 + # Test updating the port + args = { + 'port': True, + '--private': True, + '--speed': 100 + } port_mock.side_effect = [True, False] From e193691f8fe1c522675dfba533db16724c7ff7cb Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 10:34:13 -0500 Subject: [PATCH 036/168] Changes per code review notes --- SoftLayer/CLI/modules/hardware.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index a801373e6..52b7cc203 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -595,7 +595,8 @@ class CreateHardware(CLIRunnable): --chassis=CHASSIS --memory=MEMORY --os=OS --disk=SIZE... [options] Order/create a dedicated server. See 'sl hardware list-chassis' and -'sl hardware create-options' for valid options +'sl hardware create-options' for valid options. --disk can be repeated to +order multiple disks. Required: -H --hostname=HOST Host portion of the FQDN. example: server @@ -603,7 +604,7 @@ class CreateHardware(CLIRunnable): --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 mebibytes (n * 1024) + -m --memory=MEMORY Memory in gigabytes Optional: @@ -671,7 +672,7 @@ def execute(cls, client, args): order['disk_controller'] = dc_price # Set the port speed - port_speed = args.get('--network') or 10 + port_speed = args.get('--network') or '100' nic_price = cls._get_price_id_from_options(ds_options, 'nic', port_speed) From cee97675938913a4e98b3bd5962f2ef09a80767e Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 10:55:30 -0500 Subject: [PATCH 037/168] Testing new documentation format and examples --- SoftLayer/managers/hardware.py | 10 ++++++---- docs/conf.py | 9 +++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 1020b263f..3a970cbe5 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -29,10 +29,12 @@ def cancel_hardware(self, id, reason='unneeded', comment=''): """ Cancels the specified dedicated server. :param int id: The ID of the hardware to be cancelled. - :param bool immediate: If true, the hardware will be cancelled - immediately. Otherwise, it will be - scheduled to cancel on the anniversary date. - :param string reason: The reason code for the cancellation. + :param string reason: The reason code for the cancellation. This should + come from :func:`get_cancellation_reasons`. + :param string comment: An optional comment to include with the + cancellation. + + >>> cancel_hardware(123) """ reasons = self.get_cancellation_reasons() diff --git a/docs/conf.py b/docs/conf.py index bcaa37508..815a6f79f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -92,7 +92,13 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'nature' +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if on_rtd: + html_theme = 'default' + html_style = 'default.css' +else: + html_theme = 'nature' + html_style = "style.css" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -122,7 +128,6 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -html_style = "style.css" # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. From 91c0260d4f83d1416c56c7ed4b49ef9a61ae4d45 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 13:28:04 -0500 Subject: [PATCH 038/168] Documentation improvements for the hardware manager Conflicts: SoftLayer/managers/hardware.py --- SoftLayer/managers/hardware.py | 196 +++++++++++++++++++++++++++++---- 1 file changed, 174 insertions(+), 22 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 3a970cbe5..547328f14 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -21,9 +21,16 @@ def __init__(self, client): """ self.client = client + """ A valid `SoftLayer.API.Client` object that will be used for all + actions. """ self.hardware = self.client['Hardware_Server'] + """ Reference to the SoftLayer_Hardware_Server API object. """ self.account = self.client['Account'] + """ Reference to the SoftLayer_Account API object. """ self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] + """ A list of resolver functions. Used primarily by the CLI to provide + a variety of methods for uniquely identifying an object such as + hostname and IP address.""" def cancel_hardware(self, id, reason='unneeded', comment=''): """ Cancels the specified dedicated server. @@ -33,8 +40,6 @@ def cancel_hardware(self, id, reason='unneeded', comment=''): come from :func:`get_cancellation_reasons`. :param string comment: An optional comment to include with the cancellation. - - >>> cancel_hardware(123) """ reasons = self.get_cancellation_reasons() @@ -77,7 +82,7 @@ def cancel_metal(self, id, immediate=False): def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, domain=None, datacenter=None, nic_speed=None, public_ip=None, private_ip=None, **kwargs): - """ List all hardware. + """ List all hardware (servers and bare metal computing instances). :param list tags: filter based on tags :param integer cpus: filter based on number of CPUS @@ -89,6 +94,9 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, :param string public_ip: filter based on public ip address :param string private_ip: filter based on private ip address :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) + :returns: Returns an array of dictionaries representing the matching + hardware. This list will contain both dedicated servers and + bare metal computing instances """ if 'mask' not in kwargs: @@ -148,11 +156,17 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, def get_bare_metal_create_options(self): """ Retrieves the available options for creating a bare metal server. - The information for ordering bare metal instances comes from multiple - API calls. In order to make the process easier, this function will - make those calls and reformat the results into a dictionary that's - easier to manage. It's recommended that you cache these results with a - reasonable lifetime for performance reasons. + :returns: A dictionary of creation options. The categories to order are + contained within the 'categories' key. See + :func:`_parse_package_data` for detailed information. + + .. note:: + + The information for ordering bare metal instances comes from + multiple API calls. In order to make the process easier, this + function will make those calls and reformat the results into a + dictionary that's easier to manage. It's recommended that you cache + these results with a reasonable lifetime for performance reasons. """ hw_id = self._get_bare_metal_package_id() @@ -165,10 +179,13 @@ def get_available_dedicated_server_packages(self): """ Retrieves a list of packages that are available for ordering dedicated servers. - Note - This currently returns a hard coded list until the API is - updated to allow filtering on packages to just those for ordering - servers. + :returns: A list of tuples of available dedicated server packages in + the form (id, name, description) """ + + # Note - This currently returns a hard coded list until the API is + # updated to allow filtering on packages to just those for ordering + # servers. package_ids = [13, 15, 23, 25, 26, 27, 29, 32, 41, 42, 43, 44, 49, 51, 52, 53, 54, 55, 56, 57, 126, 140, 141, 142, 143, 144, 145, 146, 147, 148, 158] @@ -190,11 +207,20 @@ def get_dedicated_server_create_options(self, package_id): """ Retrieves the available options for creating a dedicated server in a specific chassis (based on package ID). - The information for ordering dedicated servers comes from multiple - API calls. In order to make the process easier, this function will - make those calls and reformat the results into a dictionary that's - easier to manage. It's recommended that you cache these results with a - reasonable lifetime for performance reasons. + :param int package_id: The package ID to retrieve the creation options + for. This should come from + :func:`get_available_dedicated_server_packages`. + :returns: A dictionary of creation options. The categories to order are + contained within the 'categories' key. See + :func:`_parse_package_data` for detailed information. + + .. note:: + + The information for ordering dedicated servers comes from multiple + API calls. In order to make the process simpler, this function will + make those calls and reformat the results into a dictionary that's + easier to manage. It's recommended that you cache these results with + a reasonable lifetime for performance reasons. """ return self._parse_package_data(package_id) @@ -202,6 +228,8 @@ def get_hardware(self, id, **kwargs): """ Get details about a hardware device :param integer id: the hardware ID + :returns: A dictionary containing a large amount of information about + the specified server. """ @@ -275,20 +303,84 @@ def change_port_speed(self, id, public, speed): return func(speed, id=id) def place_order(self, **kwargs): - """ Places an order for a piece of hardware. See _generate_create_dict - for a list of available options. + """ Places an order for a piece of hardware. See + :func:`_generate_create_dict` for a list of available options. + + .. warning:: + Due to how the ordering structure currently works, all ordering + takes place using price IDs rather than quantities. See the + following sample for an example of using HardwareManager functions + for ordering a basic server. + + .. code-block:: python + + # client is assumed to be an initialized SoftLayer.API.Client object + mgr = HardwareManager(client) + + # Package ID 32 corresponds to the 'Quad Processor, Quad Core Intel' + # package. This information can be obtained from the + # :func:`get_available_dedicated_server_packages` function. + options = mgr.get_dedicated_server_create_options(32) + + # Review the contents of options to find the information that + # applies to your order. For the sake of this example, we assume + # that your selections are a series of item IDs for each category + # organized into a key-value dictionary. + + # This contains selections for all required categories + selections = { + 'server': 542, # Quad Processor Quad Core Intel 7310 - 1.60GHz + 'pri_ip_addresses': 15, # 1 IP Address + 'notification': 51, # Email and Ticket + 'ram': 280, # 16 GB FB-DIMM Registered 533/667 + 'bandwidth': 173, # 5000 GB Bandwidth + 'lockbox': 45, # 1 GB Lockbox + 'monitoring': 49, # Host Ping + 'disk0': 14, # 500GB SATA II (for the first disk) + 'response': 52, # Automated Notification + 'port_speed': 187, # 100 Mbps Public & Private Networks + 'power_supply': 469, # Redundant Power Supplies + 'disk_controller': 487, # Non-RAID + 'vulnerability_scanner': 307, # Nessus + 'vpn_management': 309, # Unlimited SSL VPN Users + 'remote_management': 504, # Reboot / KVM over IP + 'os': 4166, # Ubuntu Linux 12.04 LTS Precise Pangolin (64 bit) + } + + args = { + 'location': 'FIRST_AVAILABLE', # Pick the first available DC + 'disks': [], + } + + for category, item_id in selections: + for item in options['categories'][category]['items']: + if item['id'] == item_id: + if 'disk' not in category and \ + 'disk_controller' != category: + args[category] = item['price_id'] + else: + args['disks'].append(item['price_id']) + + # You can call :func:`verify_order` here to test the order instead + # of actually placing it if you prefer. + result = mgr.place_order(**args) + """ create_options = self._generate_create_dict(**kwargs) return self.client['Product_Order'].placeOrder(create_options) def verify_order(self, **kwargs): """ Verifies an order for a piece of hardware without actually placing - it. See _generate_create_dict for a list of available options. + it. See :func:`_generate_create_dict` for a list of available options. """ create_options = self._generate_create_dict(**kwargs) return self.client['Product_Order'].verifyOrder(create_options) def get_cancellation_reasons(self): + """ + Returns a dictionary of valid cancellation reasons that can be used + when cancelling a dedicated server via :func:`cancel_hardware`. + """ return { 'unneeded': 'No longer needed', 'closing': 'Business closing down', @@ -308,7 +400,10 @@ def _generate_create_dict( bare_metal=None, ram=None, package_id=None, disk_controller=None): """ Translates a list of arguments into a dictionary necessary for creating - a server. NOTE - All items here must be price IDs, NOT quantities! + a server. + + .. warning:: + All items here must be price IDs, NOT quantities! :param string server: The identification string for the server to order. This will either be the CPU/Memory @@ -435,6 +530,48 @@ def _get_ids_from_ip(self, ip): return [result['id'] for result in results] def _parse_package_data(self, package_id): + """ + Parses data from the specified package into a consistent dictionary. + + The data returned by the API varies significantly from one package + to another, which means that consuming it can make your program more + complicated than desired. This function will make all necessary API + calls for the specified package ID and build the results into a + consistently formatted dictionary like so: + + result = { + 'locations': [{'delivery_information': , + 'keyname': , + 'long_name': }], + 'categories': { + 'category_code': { + 'sort': , + 'step': , + 'is_required': , + 'name': , + 'group': , + 'items': [ + { + 'id': , + 'description': , + 'sort': , + 'price_id': , + 'recurring_fee': , + 'setup_fee': , + 'hourly_recurring_fee': , + 'one_time_fee': , + 'labor_fee': , + 'capacity': , + } + ] + } + } + } + + Your code can rely upon each of those elements always being present. + Each list will contain at least one entry as well, though most will + contain more than one. + """ package = self.client['Product_Package'] results = { @@ -497,8 +634,8 @@ def _parse_package_data(self, package_id): return results def edit(self, id, userdata=None, hostname=None, domain=None, notes=None): - """ Edit hostname, domain name, notes, and/or the - user data of the hardware + """ Edit hostname, domain name, notes, and/or the user data of the + hardware Parameters set to None will be ignored and not attempted to be updated. @@ -531,6 +668,21 @@ def edit(self, id, userdata=None, hostname=None, domain=None, notes=None): def get_default_value(package_options, category): + """ Returns the default price ID for the specified category. + + This determination is made by parsing the items in the package_options + argument and finding the first item that has zero specified for every fee + field. + + .. note:: + If the category has multiple items with no fee, this will return the + first it finds and then short circuit. This may not match the default + value presented on the SoftLayer ordering portal. Additionally, this + method will return None if there are no free items in the category. + + :returns: Returns the price ID of the first free item it finds or None + if there are no free items. + """ if category not in package_options['categories']: return From 3f31da6f563364db6da3e174223f1d87309f8a5c Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 13:51:20 -0500 Subject: [PATCH 039/168] Minor organization tweaks. Expanded table of contents to two levels to improve navigation --- docs/api/client.rst | 29 ++++++++++++++++------------- docs/cli.rst | 27 ++++++++++++++++----------- docs/index.rst | 2 +- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/docs/api/client.rst b/docs/api/client.rst index 316dd545f..e1b896b1c 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -49,9 +49,24 @@ Below is an example of creating a client instance with more options. This will c verbose=True, ) +Managers +-------- +:: +For day to day operation, most users will find the managers to be the most convenient means for interacting with the API. Managers mask out a lot of the complexities of using the API into classes that provide a simpler interface to various services. These are higher-level interfaces to the SoftLayer API. + + + >>> from SoftLayer import CCIManager, Client + >>> client = Client(...) + >>> cci = CCIManager(client) + >>> cci.list_instances() + [...] + +If you need more power or functionality than the managers provide, you can make direct API calls as well. + + Making API Calls ---------------- -The SoftLayer API client for python leverages SoftLayer's XML-RPC API. It supports authentication, object masks, object filters, limits, offsets, and retrieving objects by id. The following section assumes you have a initialized client named 'client'. +For full control over your account and services, you can directly call the SoftLayer API. The SoftLayer API client for python leverages SoftLayer's XML-RPC API. It supports authentication, object masks, object filters, limits, offsets, and retrieving objects by id. The following section assumes you have a initialized client named 'client'. The best way to test our setup is to call the `getObject `_ method on the `SoftLayer_Account `_ service. :: @@ -135,18 +150,6 @@ API Reference :undoc-members: -Managers --------- -:: - - >>> from SoftLayer import CCIManager, Client - >>> client = Client(...) - >>> cci = CCIManager(client) - >>> cci.list_instances() - [...] - -Managers mask out a lot of the complexities of using the API into classes that provide a simpler interface to various services. These are higher-level interfaces to the SoftLayer API. - .. toctree:: :maxdepth: 2 :glob: diff --git a/docs/cli.rst b/docs/cli.rst index e802708c3..a522aafb6 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -69,24 +69,29 @@ To discover the available commands, simply type `sl`. :: $ sl - usage: sl [...] - sl help + usage: sl [...] + sl help + sl help sl [-h | --help] - + SoftLayer Command-line Client - - The available commands are: - firewall Firewall rule and security management - image Manages compute and flex images - ssl Manages SSL + + The available modules are: cci Manage, delete, order compute instances - dns Manage DNS config View and edit configuration for this tool + dns Manage DNS + firewall Firewall rule and security management + hardware View hardware details + bmetal Interact with bare metal instances + network Perform various network operations + help Show help + iscsi View iSCSI details + image Manages compute and flex images metadata Get details about this machine. Also available with 'my' and 'meta' nas View NAS details - iscsi View iSCSI details + ssl Manages SSL - See 'sl help ' for more information on a specific command. + 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' diff --git a/docs/index.rst b/docs/index.rst index 342cb6eb3..9df2449f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ SoftLayer API Python Client |version| This is the documentation to SoftLayer's Python API Bindings. These bindings use SoftLayer's `XML-RPC interface `_ in order to manage SoftLayer services. .. toctree:: - :maxdepth: 1 + :maxdepth: 2 :glob: install From b3c8ff4a377d429c1abd33b8167ce52fcd1cf2e8 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 14:44:52 -0500 Subject: [PATCH 040/168] Adding more CCI documentation and tweaking some hardware docs. Fixed a TOC issue I caused when rearranging the manager docs. --- SoftLayer/managers/cci.py | 84 ++++++++++++++++++++++++++++++++-- SoftLayer/managers/hardware.py | 14 +++--- docs/api/client.rst | 16 ++++--- 3 files changed, 97 insertions(+), 17 deletions(-) diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index c6833523c..c7ad247e9 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -1,5 +1,5 @@ """ - SoftLayer.CCI + SoftLayer.cci ~~~~~~~~~~~~~ CCI Manager/helpers @@ -17,9 +17,16 @@ class CCIManager(IdentifierMixin, object): """ Manage CCIs """ def __init__(self, client): self.client = client + """ A valid `SoftLayer.API.Client` object that will be used for all + actions. """ self.account = client['Account'] + """ Reference to the SoftLayer_Account API object. """ self.guest = client['Virtual_Guest'] + """ Reference to the SoftLayer_Virtual_Guest API object. """ self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] + """ A list of resolver functions. Used primarily by the CLI to provide + a variety of methods for uniquely identifying an object such as + hostname and IP address.""" def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, memory=None, hostname=None, domain=None, @@ -40,6 +47,21 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, :param string public_ip: filter based on public ip address :param string private_ip: filter based on private ip address :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) + :returns: Returns an array of dictionaries representing the matching + CCIs. + + :: + + # Print out a list of all hourly CCIs in the DAL05 data center. + # env variables + # SL_USERNAME = YOUR_USERNAME + # SL_API_KEY = YOUR_API_KEY + import SoftLayer + client = SoftLayer.Client() + + mgr = SoftLayer.CCIManager(client) + for cci in mgr.list_instances(hourly=True, datacenter='dal05'): + print cci['fullyQualifiedDomainName'], cci['primaryIpAddress'] """ if 'mask' not in kwargs: @@ -115,6 +137,8 @@ def get_instance(self, id, **kwargs): """ Get details about a CCI instance :param integer id: the instance ID + :returns: A dictionary containing a large amount of information about + the specified instance. """ @@ -157,6 +181,11 @@ def get_instance(self, id, **kwargs): return self.guest.getObject(id=id, **kwargs) def get_create_options(self): + """ Retrieves the available options for creating a CCI. + + :returns: A dictionary of creation options. + + """ return self.guest.getCreateObjectOptions() def cancel_instance(self, id): @@ -192,6 +221,37 @@ def _generate_create_dict( datacenter=None, os_code=None, image_id=None, private=False, public_vlan=None, private_vlan=None, userdata=None, nic_speed=None, disks=None, post_uri=None): + """ + Translates a list of arguments into a dictionary necessary for creating + a CCI. + + :param int cpus: The number of virtual CPUs to include in the instance. + :param int memory: The amount of RAM to order. + :param bool hourly: Flag to indicate if this server should be billed + hourly (default) or monthly. + :param string hostname: The hostname to use for the new server. + :param string domain: The domain to use for the new server. + :param bool local_disk: Flag to indicate if this should be a local disk + (default) or a SAN disk. + :param string datacenter: The short name of the data center in which + the CCI should reside. + :param string os_code: The operating system to use. Cannot be specified + if image_id is specified. + :param int image_id: The ID of the image to load onto the server. + Cannot be specified if os_code is specified. + :param bool private: Flag to indicate if this should be housed on a + private or shared host (default). This will incur + a fee on your account. + :param int public_vlan: The ID of the public VLAN on which you want + this CCI placed. + :param int private_vlan: The ID of the public VLAN on which you want + this CCI placed. + :param bool bare_metal: Flag to indicate if this is a bare metal server + or a dedicated server (default). + :param list disks: A list of disk capacities for this server. + :param string post_url: The URI of the post-install script to run + after reload + """ required = [cpus, memory, hostname, domain] @@ -265,6 +325,13 @@ def _generate_create_dict( return data def wait_for_transaction(self, id, limit, delay=1): + """ Waits on a CCI transaction for the specified amount of time. + + :param int id: The instance ID with the pending transaction + :param int limit: The maximum amount of time to wait. + :param int delay: The number of seconds to sleep before checks. + Defaults to 1. + """ for count, new_instance in enumerate(repeat(id)): instance = self.get_instance(new_instance) if not instance.get('activeTransaction', {}).get('id') and \ @@ -277,16 +344,27 @@ def wait_for_transaction(self, id, limit, delay=1): sleep(delay) def verify_create_instance(self, **kwargs): - """ see _generate_create_dict """ # TODO: document this + """ Verifies an instance creation command without actually placing an + order. See :func:`_generate_create_dict` for a list of available + options. """ create_options = self._generate_create_dict(**kwargs) return self.guest.generateOrderTemplate(create_options) def create_instance(self, **kwargs): - """ see _generate_create_dict """ # TODO: document this + """ Orders a new instance. See :func:`_generate_create_dict` for + a list of available options. """ create_options = self._generate_create_dict(**kwargs) return self.guest.createObject(create_options) def change_port_speed(self, id, public, speed): + """ Allows you to change the port speed of a CCI's NICs. + + :param int id: The ID of the CCI + :param bool public: Flag to indicate which interface to change. + True (default) means the public interface. + False indicates the private interface. + :param int speed: The port speed to set. + """ if public: func = self.guest.setPublicNetworkInterfaceSpeed else: diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 547328f14..80fd13989 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -291,8 +291,8 @@ def change_port_speed(self, id, public, speed): :param int id: The ID of the server :param bool public: Flag to indicate which interface to change. - True (default) means the public interface. - False indicates the private interface. + True (default) means the public interface. + False indicates the private interface. :param int speed: The port speed to set. """ if public: @@ -312,7 +312,7 @@ def place_order(self, **kwargs): following sample for an example of using HardwareManager functions for ordering a basic server. - .. code-block:: python + :: # client is assumed to be an initialized SoftLayer.API.Client object mgr = HardwareManager(client) @@ -405,10 +405,10 @@ def _generate_create_dict( .. warning:: All items here must be price IDs, NOT quantities! - :param string server: The identification string for the server to - order. This will either be the CPU/Memory - combination ID for bare metal instances or the - CPU model for dedicated servers. + :param int server: The identification string for the server to + order. This will either be the CPU/Memory + combination ID for bare metal instances or the + CPU model for dedicated servers. :param string hostname: The hostname to use for the new server. :param string domain: The domain to use for the new server. :param bool hourly: Flag to indicate if this server should be billed diff --git a/docs/api/client.rst b/docs/api/client.rst index e1b896b1c..746fbd525 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -61,6 +61,15 @@ For day to day operation, most users will find the managers to be the most conve >>> cci.list_instances() [...] +Available Managers: +~~~~~~~~~~~~~~~~~~~ + +.. toctree:: + :maxdepth: 2 + :glob: + + managers/* + If you need more power or functionality than the managers provide, you can make direct API calls as well. @@ -150,13 +159,6 @@ API Reference :undoc-members: -.. toctree:: - :maxdepth: 2 - :glob: - - managers/* - - Backwards Compatibility ----------------------- As of 3.0, the old API methods and parameters no longer work. Below are examples of converting the old API to the new one. From 3db1f6340e907cff106ab0bf9c57390a937793cf Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 15:17:35 -0500 Subject: [PATCH 041/168] Additional documentation updates for consistency --- SoftLayer/managers/cci.py | 3 +-- SoftLayer/managers/dns.py | 16 ++++++++++++++-- SoftLayer/managers/firewall.py | 11 +++++++++++ SoftLayer/managers/hardware.py | 2 +- SoftLayer/managers/messaging.py | 12 ++++++------ SoftLayer/managers/network.py | 16 +++++++++++++++- SoftLayer/managers/ssl.py | 19 +++++++++++++------ 7 files changed, 61 insertions(+), 18 deletions(-) diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index c7ad247e9..0fde339cd 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -47,8 +47,7 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, :param string public_ip: filter based on public ip address :param string private_ip: filter based on private ip address :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) - :returns: Returns an array of dictionaries representing the matching - CCIs. + :returns: Returns a list of dictionaries representing the matching CCIs :: diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index a98023934..f5811965b 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -1,5 +1,5 @@ """ - SoftLayer.DNS + SoftLayer.dns ~~~~~~~~~~~~~ DNS Manager/helpers @@ -22,9 +22,17 @@ def __init__(self, client): """ self.client = client + """ A valid `SoftLayer.API.Client` object that will be used for all + actions. """ self.service = self.client['Dns_Domain'] + """ Reference to the SoftLayer_Dns_Domain API object. """ self.record = self.client['Dns_Domain_ResourceRecord'] + """ Reference to the SoftLayer.Dns_Domain_ResourceRecord + API object. """ self.resolvers = [self._get_zone_id_from_name] + """ A list of resolver functions. Used primarily by the CLI to provide + a variety of methods for uniquely identifying an object such as zone + name """ def _get_zone_id_from_name(self, name): results = self.client['Account'].getDomains( @@ -35,6 +43,7 @@ def list_zones(self, **kwargs): """ Retrieve a list of all DNS zones. :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) + :returns: A list of dictionaries representing the matching zones. """ return self.client['Account'].getDomains(**kwargs) @@ -43,6 +52,8 @@ def get_zone(self, zone_id, records=True): """ Get a zone and its records. :param zone: the zone name + :returns: A dictionary containing a large amount of information about + the specified zone. """ mask = None @@ -120,7 +131,8 @@ def get_records(self, zone_id, ttl=None, data=None, host=None, :param host: optionally, record's host :param type: optionally, the type of record: - :returns list: + :returns: A list of dictionaries representing the matching records + within the specified zone. """ _filter = NestedDict() diff --git a/SoftLayer/managers/firewall.py b/SoftLayer/managers/firewall.py index 626605dd0..4e20ea106 100644 --- a/SoftLayer/managers/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -9,6 +9,11 @@ def has_firewall(vlan): + """ Helper to determine whether or not a VLAN has a firewall. + + :param dict vlan: A dictionary representing a VLAN + :returns: True if the VLAN has a firewall, false if it doesn't. + """ return bool( vlan.get('dedicatedFirewallFlag', None) or vlan.get('highAvailabilityFirewallFlag', None) or @@ -26,8 +31,14 @@ def __init__(self, client): """ self.client = client + """ A valid `SoftLayer.API.Client` object that will be used for all + actions. """ def get_firewalls(self): + """ Returns a list of all firewalls on the account. + + :returns: A list of firewalls on the current account. + """ results = self.client['Account'].getObject( mask={ 'networkVlans': { diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 80fd13989..dccb59312 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -94,7 +94,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, :param string public_ip: filter based on public ip address :param string private_ip: filter based on private ip address :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) - :returns: Returns an array of dictionaries representing the matching + :returns: Returns a list of dictionaries representing the matching hardware. This list will contain both dedicated servers and bare metal computing instances diff --git a/SoftLayer/managers/messaging.py b/SoftLayer/managers/messaging.py index ad5d7e9be..2fce07a93 100644 --- a/SoftLayer/managers/messaging.py +++ b/SoftLayer/managers/messaging.py @@ -161,7 +161,8 @@ def authenticate(self, username, api_key, auth_token=None): :param api_key: SoftLayer API Key :param auth_token: (optional) Starting auth token """ - auth_endpoint = '/'.join((self.endpoint, 'v1', self.account_id, 'auth')) + auth_endpoint = '/'.join((self.endpoint, 'v1', + self.account_id, 'auth')) auth = QueueAuth(auth_endpoint, username, api_key, auth_token=auth_token) auth.auth() @@ -239,7 +240,7 @@ def push_queue_message(self, queue_name, body, **kwargs): message = {'body': body} message.update(kwargs) r = self._make_request('post', 'queues/%s/messages' % queue_name, - data=json.dumps(message)) + data=json.dumps(message)) return json.loads(r.content) def pop_message(self, queue_name, count=1): @@ -249,7 +250,7 @@ def pop_message(self, queue_name, count=1): :param count: (optional) number of messages to retrieve """ r = self._make_request('get', 'queues/%s/messages' % queue_name, - params={'batch': count}) + params={'batch': count}) return json.loads(r.content) def delete_message(self, queue_name, message_id): @@ -259,7 +260,7 @@ def delete_message(self, queue_name, message_id): :param message_id: Message id """ self._make_request('delete', 'queues/%s/messages/%s' - % (queue_name, message_id)) + % (queue_name, message_id)) return True # TOPIC METHODS @@ -324,7 +325,7 @@ def push_topic_message(self, topic_name, body, **kwargs): message = {'body': body} message.update(kwargs) r = self._make_request('post', 'topics/%s/messages' % topic_name, - data=json.dumps(message)) + data=json.dumps(message)) return json.loads(r.content) def get_subscriptions(self, topic_name): @@ -356,4 +357,3 @@ def delete_subscription(self, topic_name, subscription_id): self._make_request('delete', 'topics/%s/subscriptions/%s' % (topic_name, subscription_id)) return True - diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 600260203..44682be9f 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -1,5 +1,5 @@ """ - SoftLayer.Network + SoftLayer.network ~~~~~~~~~~~~~~~~~ Network Manager/helpers @@ -14,13 +14,19 @@ class NetworkManager(IdentifierMixin, object): """ Manage Networks """ def __init__(self, client): self.client = client + """ A valid `SoftLayer.API.Client` object that will be used for all + actions. """ self.account = client['Account'] + """ Reference to the SoftLayer_Account API object. """ self.vlan = client['Network_Vlan'] + """ Reference to the SoftLayer_Network_Vlan object. """ def get_vlan(self, id): """ Returns information about a single VLAN. :param int id: The unique identifier for the VLAN + :returns: A dictionary containing a large amount of information about + the specified VLAN. """ return self.vlan.getObject(id=id, mask=self._get_vlan_mask()) @@ -55,6 +61,14 @@ def summary_by_datacenter(self): """ Provides a dictionary with a summary of all network information on the account, grouped by data center. + The resultant dictionary is primarily useful for statistical purposes. + It contains count information rather than raw data. If you want raw + information, see the :func:`list_vlans` method instead. + + :returns: A dictionary keyed by data center with the data containing a + series of counts for hardware, subnets, CCIs, and other + objects residing within that data center. + """ datacenters = {} for vlan in self._get_vlans(): diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index 2d4f4b9d3..7c5fc8570 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -1,5 +1,5 @@ """ - SoftLayer.SSL + SoftLayer.ssl ~~~~~~~~~~~~~ SSL Manager/helpers @@ -18,12 +18,17 @@ def __init__(self, client): """ self.client = client + """ A valid `SoftLayer.API.Client` object that will be used for all + actions. """ self.ssl = self.client['Security_Certificate'] + """ Reference to the SoftLayer_Security_Certificate API object. """ def list_certs(self, method='all'): """ List all certificates. - :param method: # TODO: explain this param + :param string method: The type of certificates to list. Options are + 'all', 'expired', and 'valid'. + :returns: A list of dictionaries representing the requested SSL certs. """ ssl = self.client['Account'] @@ -40,7 +45,8 @@ def list_certs(self, method='all'): def add_certificate(self, certificate): """ Creates a new certificate. - :param certificate: # TODO: is this a dict? + :param dict certificate: A dictionary representing the parts of the + certificate. See SLDN for more information. """ return self.ssl.createObject(certificate) @@ -54,9 +60,10 @@ def remove_certificate(self, id): return self.ssl.deleteObject(id=id) def edit_certificate(self, certificate): - """ Updates a certificate with the included options. The provided dict - must include an 'id' key and value corresponding to the certificate ID - that should be updated. + """ Updates a certificate with the included options. + + The provided dict must include an 'id' key and value corresponding to + the certificate ID that should be updated. :param dict certificate: the certificate to update. From 3fd1e30384446fb94524f70d9a10c88f4099e522 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 15:20:23 -0500 Subject: [PATCH 042/168] Attempting to tweak styles on ReadTheDocs --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 815a6f79f..68f2c5d33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,7 +95,7 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: html_theme = 'default' - html_style = 'default.css' + html_style = None else: html_theme = 'nature' html_style = "style.css" From 0c323f1ac8ab2b1396298a3d34ae246ebae2b640 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 5 Aug 2013 15:22:17 -0500 Subject: [PATCH 043/168] More styling attempts --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 68f2c5d33..1933ddc53 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,7 +95,6 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if on_rtd: html_theme = 'default' - html_style = None else: html_theme = 'nature' html_style = "style.css" From c2a893c49c2d25982eae8292e8c77df2df602361 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 6 Aug 2013 09:26:03 -0500 Subject: [PATCH 044/168] Updating attribute documentation --- SoftLayer/managers/cci.py | 14 +++++++------- SoftLayer/managers/dns.py | 25 ++++++++++++------------- SoftLayer/managers/firewall.py | 12 ++++++------ SoftLayer/managers/hardware.py | 25 ++++++++++++------------- SoftLayer/managers/network.py | 8 ++++---- SoftLayer/managers/ssl.py | 17 ++++++++--------- 6 files changed, 49 insertions(+), 52 deletions(-) diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 0fde339cd..40819ee53 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -16,17 +16,17 @@ class CCIManager(IdentifierMixin, object): """ Manage CCIs """ def __init__(self, client): + #: A valid `SoftLayer.API.Client` object that will be used for all + #: actions. self.client = client - """ A valid `SoftLayer.API.Client` object that will be used for all - actions. """ + #: Reference to the SoftLayer_Account API object. self.account = client['Account'] - """ Reference to the SoftLayer_Account API object. """ + #: Reference to the SoftLayer_Virtual_Guest API object. self.guest = client['Virtual_Guest'] - """ Reference to the SoftLayer_Virtual_Guest API object. """ + #: A list of resolver functions. Used primarily by the CLI to provide + #: a variety of methods for uniquely identifying an object such as + #: hostname and IP address. self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] - """ A list of resolver functions. Used primarily by the CLI to provide - a variety of methods for uniquely identifying an object such as - hostname and IP address.""" def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, memory=None, hostname=None, domain=None, diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index f5811965b..a343f4f05 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -13,26 +13,25 @@ class DNSManager(IdentifierMixin, object): - """ Manage DNS zones. """ + """ DNSManager initialization. - def __init__(self, client): - """ DNSManager initialization. + :param SoftLayer.API.Client client: the client instance - :param SoftLayer.API.Client client: the client instance + """ - """ + def __init__(self, client): + #: A valid `SoftLayer.API.Client` object that will be used for all + #: actions. self.client = client - """ A valid `SoftLayer.API.Client` object that will be used for all - actions. """ + #: Reference to the SoftLayer_Dns_Domain API object. self.service = self.client['Dns_Domain'] - """ Reference to the SoftLayer_Dns_Domain API object. """ + #: Reference to the SoftLayer.Dns_Domain_ResourceRecord + #: API object. self.record = self.client['Dns_Domain_ResourceRecord'] - """ Reference to the SoftLayer.Dns_Domain_ResourceRecord - API object. """ + #: A list of resolver functions. Used primarily by the CLI to provide + #: a variety of methods for uniquely identifying an object such as zone + #: name. self.resolvers = [self._get_zone_id_from_name] - """ A list of resolver functions. Used primarily by the CLI to provide - a variety of methods for uniquely identifying an object such as zone - name """ def _get_zone_id_from_name(self, name): results = self.client['Account'].getDomains( diff --git a/SoftLayer/managers/firewall.py b/SoftLayer/managers/firewall.py index 4e20ea106..f38f1fb0c 100644 --- a/SoftLayer/managers/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -24,15 +24,15 @@ def has_firewall(vlan): class FirewallManager(object): - def __init__(self, client): - """ Manages firewalls. + """ Manages firewalls. - :param SoftLayer.API.Client client: the API client instance + :param SoftLayer.API.Client client: the API client instance - """ + """ + def __init__(self, client): + #: A valid `SoftLayer.API.Client` object that will be used for all + #: actions. self.client = client - """ A valid `SoftLayer.API.Client` object that will be used for all - actions. """ def get_firewalls(self): """ Returns a list of all firewalls on the account. diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index dccb59312..ed3ab8bfd 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -12,25 +12,24 @@ class HardwareManager(IdentifierMixin, object): - """ Manages hardware devices. """ - - def __init__(self, client): - """ HardwareManager initialization. + """ + Manages hardware devices. - :param SoftLayer.API.Client client: an API client instance + :param SoftLayer.API.Client client: an API client instance + """ - """ + def __init__(self, client): + #: A valid `SoftLayer.API.Client` object that will be used for all + #: actions. self.client = client - """ A valid `SoftLayer.API.Client` object that will be used for all - actions. """ + #: Reference to the SoftLayer_Hardware_Server API object. self.hardware = self.client['Hardware_Server'] - """ Reference to the SoftLayer_Hardware_Server API object. """ + #: Reference to the SoftLayer_Account API object. self.account = self.client['Account'] - """ Reference to the SoftLayer_Account API object. """ + #: A list of resolver functions. Used primarily by the CLI to provide + #: a variety of methods for uniquely identifying an object such as + #: hostname and IP address. self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] - """ A list of resolver functions. Used primarily by the CLI to provide - a variety of methods for uniquely identifying an object such as - hostname and IP address.""" def cancel_hardware(self, id, reason='unneeded', comment=''): """ Cancels the specified dedicated server. diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 44682be9f..37643f8fc 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -13,13 +13,13 @@ class NetworkManager(IdentifierMixin, object): """ Manage Networks """ def __init__(self, client): + #: A valid `SoftLayer.API.Client` object that will be used for all + #: actions. self.client = client - """ A valid `SoftLayer.API.Client` object that will be used for all - actions. """ + #: Reference to the SoftLayer_Account API object. self.account = client['Account'] - """ Reference to the SoftLayer_Account API object. """ + #: Reference to the SoftLayer_Network_Vlan object. self.vlan = client['Network_Vlan'] - """ Reference to the SoftLayer_Network_Vlan object. """ def get_vlan(self, id): """ Returns information about a single VLAN. diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index 7c5fc8570..f967c685c 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -9,19 +9,18 @@ class SSLManager(object): - """ Manages SSL certificates. """ + """ + Manages SSL certificates. - def __init__(self, client): - """ SSLManager initialization. - - :param SoftLayer.API.Client client: an API client instance + :param SoftLayer.API.Client client: an API client instance + """ - """ + def __init__(self, client): + #: A valid `SoftLayer.API.Client` object that will be used for all + #: actions. self.client = client - """ A valid `SoftLayer.API.Client` object that will be used for all - actions. """ + #: Reference to the SoftLayer_Security_Certificate API object. self.ssl = self.client['Security_Certificate'] - """ Reference to the SoftLayer_Security_Certificate API object. """ def list_certs(self, method='all'): """ List all certificates. From 02b0958f470e2043cea12bf203bf8d404aa0edfb Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 6 Aug 2013 13:32:53 -0500 Subject: [PATCH 045/168] Tweaks Documentation * Adds separate page that details the CLI config file format. It wasn't appropriate in-line where it was. * Adds coveralls support * Adds badges for pypi downloads and pypi version * Converts README.md to rst format and renames to README.rst. rst is the main format supported on PyPi. * Consolidate unittest2 import logic --- .travis.yml | 1 - MANIFEST.in | 2 +- README.md => README.rst | 38 +++++++++++--------- SoftLayer/CLI/core.py | 2 +- SoftLayer/tests/CLI/core_tests.py | 5 +-- SoftLayer/tests/CLI/environment_tests.py | 5 +-- SoftLayer/tests/CLI/helper_tests.py | 5 +-- SoftLayer/tests/__init__.py | 5 +++ SoftLayer/tests/api_tests.py | 6 +--- SoftLayer/tests/auth_tests.py | 6 +--- SoftLayer/tests/basic_tests.py | 6 +--- SoftLayer/tests/functional_tests.py | 13 +++---- SoftLayer/tests/managers/cci_tests.py | 5 +-- SoftLayer/tests/managers/dns_tests.py | 5 +-- SoftLayer/tests/managers/firewall_tests.py | 5 +-- SoftLayer/tests/managers/hardware_tests.py | 6 +--- SoftLayer/tests/managers/metadata_tests.py | 5 +-- SoftLayer/tests/managers/network_tests.py | 7 ++-- SoftLayer/tests/managers/queue_tests.py | 9 ++--- SoftLayer/tests/managers/ssl_tests.py | 5 +-- SoftLayer/tests/transport_tests.py | 5 +-- docs/cli.rst | 42 ++++++++-------------- docs/cli/config_file.rst | 19 ++++++++++ docs/index.rst | 11 ++++++ requirements.txt | 3 +- setup.py | 4 +-- 26 files changed, 97 insertions(+), 128 deletions(-) rename README.md => README.rst (51%) create mode 100644 docs/cli/config_file.rst diff --git a/.travis.yml b/.travis.yml index ff4a0f909..8fb11f473 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,3 @@ install: - "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi" # command to run tests script: python setup.py nosetests - diff --git a/MANIFEST.in b/MANIFEST.in index 689e50f5b..9d5d250d0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include LICENSE -include README.md \ No newline at end of file +include README.rst diff --git a/README.md b/README.rst similarity index 51% rename from README.md rename to README.rst index e980b20c2..72ef216b0 100644 --- a/README.md +++ b/README.rst @@ -1,30 +1,34 @@ SoftLayer API Python Client =========================== -SoftLayer API bindings for Python. For use with -[SoftLayer's API](http://sldn.softlayer.com/reference/softlayerapi). - * [Module Documentation](http://softlayer.github.com/softlayer-api-python-client) - * [API Documentation](http://softlayer.github.com/softlayer-api-python-client/client.html) - * [CLI Documentation](http://softlayer.github.com/softlayer-api-python-client/cli.html) +.. image:: https://badge.fury.io/py/SoftLayer.png + :target: http://badge.fury.io/py/SoftLayer -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](http://sldn.softlayer.com/article/Using-Object-Masks-SoftLayerAPI). -It also contains a command-line interface that can be used to access various -SoftLayer services using the API. +.. image:: https://pypip.in/d/SoftLayer/badge.png + :target: https://crate.io/packages/SoftLayer +SoftLayer API bindings for Python. For use with `SoftLayer's API `_. + +This library provides a simple interface to interact with SoftLayer's XML-RPC API and provides support for many of SoftLayer API's features like `object masks `_ and a command-line interface that can be used to access various SoftLayer services using the API. + +Documentation +------------- +Documentation is available at http://softlayer.github.com/softlayer-api-python-client + Installation ------------ Install via pip: -``` -pip install softlayer -``` + +.. code-block:: bash + + $ pip install softlayer + Or you can install from source. Download source and run: -``` -python setup.py install -``` +.. code-block:: bash + + $ python setup.py install The most up to date version of this library can be found on the SoftLayer @@ -43,5 +47,5 @@ System Requirements Copyright --------- -This software is Copyright (c) 2013 [SoftLayer Technologies, Inc](http://www.softlayer.com/). +This software is Copyright (c) 2013 SoftLayer Technologies, Inc. See the bundled LICENSE file for more information. diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 17c1c498d..be8ceaec7 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -175,7 +175,7 @@ def main(args=sys.argv[1:], env=Environment()): if s: env.out(s) - except InvalidCommand, e: + except InvalidCommand as e: env.err(resolver.get_module_help(e.module_name)) if e.command_name: env.err('') diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 2d344dec6..0f4bab59e 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -5,14 +5,11 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import MagicMock, patch import SoftLayer import SoftLayer.CLI as cli +from SoftLayer.tests import unittest from SoftLayer.CLI.helpers import CLIAbort from SoftLayer.CLI.environment import Environment, InvalidModule diff --git a/SoftLayer/tests/CLI/environment_tests.py b/SoftLayer/tests/CLI/environment_tests.py index 0c1ec5c71..650131fc5 100644 --- a/SoftLayer/tests/CLI/environment_tests.py +++ b/SoftLayer/tests/CLI/environment_tests.py @@ -7,13 +7,10 @@ """ import sys import os -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import patch, MagicMock from SoftLayer import API_PUBLIC_ENDPOINT +from SoftLayer.tests import unittest from SoftLayer.CLI.environment import Environment, InvalidCommand if sys.version_info >= (3,): diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 29d1ddf08..b57b3656d 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -8,13 +8,10 @@ import sys import os import json -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import patch import SoftLayer.CLI as cli +from SoftLayer.tests import unittest if sys.version_info >= (3,): raw_input_path = 'builtins.input' diff --git a/SoftLayer/tests/__init__.py b/SoftLayer/tests/__init__.py index e69de29bb..3f65687f3 100644 --- a/SoftLayer/tests/__init__.py +++ b/SoftLayer/tests/__init__.py @@ -0,0 +1,5 @@ + +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 950849988..0424b51b4 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -5,15 +5,11 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA - from mock import patch, call import SoftLayer import SoftLayer.API +from SoftLayer.tests import unittest from SoftLayer.consts import USER_AGENT diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py index 6c0f0bf48..9a0be055a 100644 --- a/SoftLayer/tests/auth_tests.py +++ b/SoftLayer/tests/auth_tests.py @@ -5,13 +5,9 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA - from SoftLayer.auth import ( AuthenticationBase, BasicAuthentication, TokenAuthentication) +from SoftLayer.tests import unittest class TestAuthenticationBase(unittest.TestCase): diff --git a/SoftLayer/tests/basic_tests.py b/SoftLayer/tests/basic_tests.py index 6cdad6aa6..44b59c8c6 100644 --- a/SoftLayer/tests/basic_tests.py +++ b/SoftLayer/tests/basic_tests.py @@ -6,12 +6,8 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA - import SoftLayer +from SoftLayer.tests import unittest class TestExceptions(unittest.TestCase): diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py index a4a2742b2..d1bd9f7af 100644 --- a/SoftLayer/tests/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -5,13 +5,10 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer import os -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA +import SoftLayer +from SoftLayer.tests import unittest def get_creds(): @@ -47,7 +44,7 @@ def test_404(self): self.assertEqual(e.faultCode, 404) self.assertIn('NOT FOUND', e.faultString) self.assertIn('NOT FOUND', e.reason) - except: + else: self.fail('No Exception Raised') def test_no_hostname(self): @@ -59,7 +56,7 @@ def test_no_hostname(self): self.assertEqual(e.faultCode, 0) self.assertIn('not known', e.faultString) self.assertIn('not known', e.reason) - except: + else: self.fail('No Exception Raised') @@ -78,7 +75,7 @@ def test_service_does_not_exist(self): self.assertEqual(e.faultCode, '-32601') self.assertEqual(e.faultString, 'Service does not exist') self.assertEqual(e.reason, 'Service does not exist') - except: + else: self.fail('No Exception Raised') def test_dns(self): diff --git a/SoftLayer/tests/managers/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py index 4717c60f6..ecb1c876f 100644 --- a/SoftLayer/tests/managers/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -6,11 +6,8 @@ :license: BSD, see LICENSE for more details. """ from SoftLayer import CCIManager +from SoftLayer.tests import unittest -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import MagicMock, ANY, call, patch diff --git a/SoftLayer/tests/managers/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py index d3fc77b5f..1f6a7aa2b 100644 --- a/SoftLayer/tests/managers/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -6,11 +6,8 @@ :license: BSD, see LICENSE for more details. """ from SoftLayer import DNSManager, DNSZoneNotFound +from SoftLayer.tests import unittest -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import MagicMock, ANY diff --git a/SoftLayer/tests/managers/firewall_tests.py b/SoftLayer/tests/managers/firewall_tests.py index e5a1c32a7..d90dd866d 100644 --- a/SoftLayer/tests/managers/firewall_tests.py +++ b/SoftLayer/tests/managers/firewall_tests.py @@ -6,11 +6,8 @@ :license: BSD, see LICENSE for more details. """ from SoftLayer import FirewallManager +from SoftLayer.tests import unittest -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import MagicMock, ANY diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 40ff9cd8a..7b211f3fd 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -7,11 +7,8 @@ """ from SoftLayer import HardwareManager from SoftLayer.managers.hardware import get_default_value +from SoftLayer.tests import unittest -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import MagicMock, ANY, call, patch @@ -367,4 +364,3 @@ def _setup_package_mocks(self, package_id): 'packageId': package_id, }], }] - \ No newline at end of file diff --git a/SoftLayer/tests/managers/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py index c54f8529d..04afa38db 100644 --- a/SoftLayer/tests/managers/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -7,11 +7,8 @@ """ from SoftLayer import MetadataManager, SoftLayerError, SoftLayerAPIError from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST +from SoftLayer.tests import unittest -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import patch, MagicMock diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 14e59731d..26c571a0d 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -6,12 +6,9 @@ :license: BSD, see LICENSE for more details. """ from SoftLayer import NetworkManager +from SoftLayer.tests import unittest -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA -from mock import MagicMock, ANY, call, patch +from mock import MagicMock, ANY, call class NetworkTests(unittest.TestCase): diff --git a/SoftLayer/tests/managers/queue_tests.py b/SoftLayer/tests/managers/queue_tests.py index d95fcf8fc..eece9c0d9 100644 --- a/SoftLayer/tests/managers/queue_tests.py +++ b/SoftLayer/tests/managers/queue_tests.py @@ -8,12 +8,9 @@ from SoftLayer import MessagingManager, Unauthenticated import SoftLayer.managers.messaging from SoftLayer.consts import USER_AGENT +from SoftLayer.tests import unittest import json -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import MagicMock, patch, ANY QUEUE_1 = { @@ -45,7 +42,6 @@ SUBSCRIPTION_LIST = {'item_count': 1, 'items': [SUBSCRIPTION_1]} - def mocked_auth_call(self): self.auth_token = 'NEW_AUTH_TOKEN' @@ -117,7 +113,6 @@ def test_call_unauthed(self): self.assertEqual(request.headers, {'X-Auth-Token': 'NEW_AUTH_TOKEN'}) - class MessagingManagerTests(unittest.TestCase): def setUp(self): @@ -224,7 +219,7 @@ def test_authenticate(self, auth): @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_stats(self, make_request): content = { - 'notifications': [{'key': [2012, 7, 27, 14, 31], 'value': 2}], + 'notifications': [{'key': [2012, 7, 27, 14, 31], 'value': 2}], 'requests': [{'key': [2012, 7, 27, 14, 31], 'value': 11}]} make_request().content = json.dumps(content) result = self.conn.stats() diff --git a/SoftLayer/tests/managers/ssl_tests.py b/SoftLayer/tests/managers/ssl_tests.py index b8ddd2124..925853c61 100644 --- a/SoftLayer/tests/managers/ssl_tests.py +++ b/SoftLayer/tests/managers/ssl_tests.py @@ -6,11 +6,8 @@ :license: BSD, see LICENSE for more details. """ from SoftLayer import SSLManager +from SoftLayer.tests import unittest -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import MagicMock, ANY diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index f64aff440..f2c812df0 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -5,14 +5,11 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA from mock import patch, MagicMock from SoftLayer import SoftLayerAPIError, TransportError from SoftLayer.transport import make_rest_api_call, make_xml_rpc_api_call +from SoftLayer.tests import unittest from requests import HTTPError, RequestException diff --git a/docs/cli.rst b/docs/cli.rst index a522aafb6..d3e64dacd 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -12,15 +12,24 @@ The SoftLayer command line interface is available via the `sl` command available cli/dev +.. _config_setup: + Configuration Setup ------------------- -To check the configuration, you can use `sl config show`. +To update the configuration, you can use `sl config setup`. :: $ sl config setup - Username: username - API Key: oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha - Endpoint URL [https://api.softlayer.com/xmlrpc/v3/]: + Username []: username + API Key or Password []: + Endpoint (public|private|custom): public + :..............:..................................................................: + : Name : Value : + :..............:..................................................................: + : Username : username : + : API Key : oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha : + : Endpoint URL : https://api.softlayer.com/xmlrpc/v3/ : + :..............:..................................................................: Are you sure you want to write settings to "/path/to/home/.softlayer"? [y/N]: y To check the configuration, you can use `sl config show`. @@ -36,30 +45,7 @@ To check the configuration, you can use `sl config show`. :..............:..................................................................: -Configuration File Details --------------------------- -The CLI loads your settings from a number of different locations. - -* Enviorment variables (SL_USERNAME, SL_API_KEY) -* Config file (~/.softlayer) -* Or argument (-C/path/to/config or --config=/path/to/config) - -The configuration file is INI based and requires the `softlayer` section to be present. -The only required fields are `username` and `api_key`. You can optionally also/exclusively supply the `endpoint_url` as well. - -*Full config* -:: - - [softlayer] - username = username - api_key = oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha - endpoint_url = https://api.softlayer.com/xmlrpc/v3/ - -*exclusive url* -:: - - [softlayer] - endpoint_url = https://api.softlayer.com/xmlrpc/v3/ +To see more about the config file format, see :ref:`config_file`. .. _usage-examples: diff --git a/docs/cli/config_file.rst b/docs/cli/config_file.rst new file mode 100644 index 000000000..48e9d771a --- /dev/null +++ b/docs/cli/config_file.rst @@ -0,0 +1,19 @@ +.. _config_file: + +CLI Configuration File +====================== +The CLI loads your settings from a number of different locations. + +* Enviorment variables (SL_USERNAME, SL_API_KEY) +* Config file (~/.softlayer) +* Or argument (-C/path/to/config or --config=/path/to/config) + +The configuration file is INI-based and requires the `softlayer` section to be present. The only required fields are `username` and `api_key`. You can optionally supply the `endpoint_url` as well. This file is created automatically by the `sl config setup` command detailed here: :ref:`config_setup`. + +*Full config* +:: + + [softlayer] + username = username + api_key = oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha + endpoint_url = https://api.softlayer.com/xmlrpc/v3/ diff --git a/docs/index.rst b/docs/index.rst index 9df2449f8..eca77ac6a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,6 +2,14 @@ SoftLayer API Python Client |version| ======================================== +`API Docs `_ ``|`` +`Github `_ ``|`` +`Issues `_ ``|`` +`PyPI `_ ``|`` +`Twitter `_ ``|`` +irc:#softlayer + + This is the documentation to SoftLayer's Python API Bindings. These bindings use SoftLayer's `XML-RPC interface `_ in order to manage SoftLayer services. .. toctree:: @@ -12,10 +20,13 @@ This is the documentation to SoftLayer's Python API Bindings. These bindings use api/client cli + External Links -------------- .. toctree:: SoftLayer API Documentation Source on Github + Issues + PyPI Twitter diff --git a/requirements.txt b/requirements.txt index 8b6f251c2..c2d4e3909 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ tox nose mock -coverage -sphinx \ No newline at end of file +coverage \ No newline at end of file diff --git a/setup.py b/setup.py index 701c6391d..ce22f0a30 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,8 @@ description = "A library to use SoftLayer's API" -if os.path.exists('README.md'): - f = open('README.md') +if os.path.exists('README.rst'): + f = open('README.rst') try: long_description = f.read() finally: From 7157df39306b6b1beda5f904224200552d85629a Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 7 Aug 2013 15:40:53 -0500 Subject: [PATCH 046/168] Fixing markup error --- docs/api/client.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api/client.rst b/docs/api/client.rst index 746fbd525..7ff6aa15f 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -51,9 +51,8 @@ Below is an example of creating a client instance with more options. This will c Managers -------- -:: For day to day operation, most users will find the managers to be the most convenient means for interacting with the API. Managers mask out a lot of the complexities of using the API into classes that provide a simpler interface to various services. These are higher-level interfaces to the SoftLayer API. - +:: >>> from SoftLayer import CCIManager, Client >>> client = Client(...) From 924bf0a0c94fee4f301d43737f314298341dae57 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 7 Aug 2013 15:41:36 -0500 Subject: [PATCH 047/168] Update README doc link to point to readthedocs --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 72ef216b0..441d1c298 100644 --- a/README.rst +++ b/README.rst @@ -13,8 +13,8 @@ This library provides a simple interface to interact with SoftLayer's XML-RPC AP Documentation ------------- -Documentation is available at http://softlayer.github.com/softlayer-api-python-client - +Documentation is available at https://softlayer-api-python-client.readthedocs.org/ + Installation ------------ Install via pip: From 2723f10d19a06c3f9869f92b61365fbffb89c0b6 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 6 Aug 2013 10:47:15 -0500 Subject: [PATCH 048/168] Adding in subnet functionality --- SoftLayer/CLI/modules/network.py | 116 ++++++++++++++++++++++++++++++- SoftLayer/managers/network.py | 81 ++++++++++++++++++++- 2 files changed, 195 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index e1886166d..613127097 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -5,7 +5,10 @@ The available commands are: summary Provide a summary view of the network - vlan Manage VLAN options + subnet-detail Display detailed information about a subnet + subnet-list Show a list of all subnets on the network + vlan-detail Display detailed information about a VLAN + vlan-list Show a list of all VLANs on the network """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. @@ -58,6 +61,117 @@ def execute(client, args): return t +class SubnetDetail(CLIRunnable): + """ +usage: sl network subnet-detail [options] + +Get detailed information about objects assigned to a particular subnet + +Filters: + --no-cci Hide CCI listing + --no-hardware Hide hardware listing +""" + action = 'subnet-detail' + + @staticmethod + def execute(client, args): + mgr = NetworkManager(client) + + subnet = mgr.get_subnet(args.get('')) + + t = Table(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + + t.add_row(['id', subnet['id']]) + t.add_row(['identifier', subnet['networkIdentifier']]) + t.add_row(['subnet type', subnet['subnetType']]) + t.add_row(['gateway', subnet['gateway']]) + t.add_row(['broadcast', subnet['broadcastAddress']]) + t.add_row(['datacenter', subnet['datacenter']['name']]) + t.add_row(['usable ips', subnet['usableIpAddressCount']]) + + if not args.get('--no-cci'): + if subnet['virtualGuests']: + cci_table = Table(['Hostname', 'Domain', 'IP']) + cci_table.align['Hostname'] = 'r' + cci_table.align['IP'] = 'l' + for cci in subnet['virtualGuests']: + cci_table.add_row([cci['hostname'], + cci['domain'], + cci['primaryIpAddress']]) + t.add_row(['ccis', cci_table]) + else: + t.add_row(['cci', 'none']) + + if not args.get('--no-hardware'): + if subnet['hardware']: + hw_table = Table(['Hostname', 'Domain', 'IP']) + hw_table.align['Hostname'] = 'r' + hw_table.align['IP'] = 'l' + for hw in subnet['hardware']: + hw_table.add_row([hw['hostname'], + hw['domain'], + hw['primaryIpAddress']]) + t.add_row(['hardware', hw_table]) + else: + t.add_row(['hardware', 'none']) + + return t + + +class SubnetList(CLIRunnable): + """ +usage: sl network subnet-list [options] + +Displays a list of subnets + +Options: + --sortby=ARG Column to sort by. options: id, number, datacenter, IPs, + hardware, ccis, networking + +Filters: + -d DC, --datacenter=DC datacenter shortname (sng01, dal05, ...) + --v4 Display only IPV4 subnets + --v6 Display only IPV6 subnets +""" + action = 'subnet-list' + + @staticmethod + def execute(client, args): + mgr = NetworkManager(client) + + t = Table([ + 'id', 'identifier', 'datacenter', 'vlan id', 'IPs', 'hardware', + 'ccis', + ]) + t.sortby = args.get('--sortby') or 'id' + + version = 0 + if args.get('--v4'): + version = 4 + elif args.get('--v6'): + version = 6 + + subnets = mgr.list_subnets( + datacenter=args.get('--datacenter'), + version=version, + ) + + for subnet in subnets: + t.add_row([ + subnet['id'], + subnet['networkIdentifier'], + subnet['datacenter']['name'], + subnet['networkVlanId'], + subnet['ipAddressCount'], + len(subnet['hardware']), + len(subnet['virtualGuests']), + ]) + + return t + + class VlanDetail(CLIRunnable): """ usage: sl network vlan-detail [options] diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 37643f8fc..da61acde2 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -7,7 +7,8 @@ :license: BSD, see LICENSE for more details. """ -from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin +from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin, \ + resolve_ids class NetworkManager(IdentifierMixin, object): @@ -20,6 +21,8 @@ def __init__(self, client): self.account = client['Account'] #: Reference to the SoftLayer_Network_Vlan object. self.vlan = client['Network_Vlan'] + self.subnet = client['Network_Subnet'] + self.subnet_resolvers = [self._get_subnet_by_identifier] def get_vlan(self, id): """ Returns information about a single VLAN. @@ -31,6 +34,17 @@ def get_vlan(self, id): """ return self.vlan.getObject(id=id, mask=self._get_vlan_mask()) + def get_subnet(self, id): + """ Returns information about a single subnet. + + :param string id: Either the ID for the subnet or its network + identifier + :returns: A dictionary of information about the subnet + """ + id = resolve_ids(id, self.subnet_resolvers)[0] + return self.subnet.getObject(id=id, mask='mask[%s]' % + ','.join(self._get_subnet_mask())) + def list_vlans(self, datacenter=None, vlan_number=None, **kwargs): """ Display a list of all VLANs on the account. @@ -57,6 +71,48 @@ def list_vlans(self, datacenter=None, vlan_number=None, **kwargs): return self._get_vlans(**kwargs) + def list_subnets(self, identifier=None, datacenter=None, version=0, + **kwargs): + """ Display a list of all subnets on the account. + + This provides a quick overview of all subnets including information + about data center residence and the number of devices attached. + + :param string datacenter: If specified, the list will only contain + subnets in the specified data center. + :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) + + """ + if 'mask' not in kwargs: + mask = self._get_subnet_mask() + kwargs['mask'] = 'mask[%s]' % ','.join(mask) + + _filter = NestedDict(kwargs.get('filter') or {}) + + # TODO - I don't think filtering works on subnets in the API + #if identifier: + # _filter['networkIdentifier'] = query_filter(identifier) + #if datacenter: + # _filter['networkVlans']['primaryRouter']['datacenter']['name'] = \ + # query_filter(datacenter) + # if version: + # _filter['version'] = query_filter(version) + + kwargs['filter'] = _filter.to_dict() + + results = self.account.getSubnets(**kwargs) + + if any([version, identifier, datacenter]): + if version: + results = filter(lambda x: x['version'] == version, results) + if identifier: + results = filter(lambda x: x['networkIdentifier'] == + identifier, results) + if datacenter: + results = filter(lambda x: x['datacenter']['name'] == + datacenter, results) + return results + def summary_by_datacenter(self): """ Provides a dictionary with a summary of all network information on the account, grouped by data center. @@ -96,6 +152,15 @@ def summary_by_datacenter(self): return datacenters + def _get_subnet_by_identifier(self, identifier): + """ Returns the ID of the subnet matching the specified identifier. + + :param string identifier: The identifier to look up + :returns: The ID of the matching subnet or None + """ + results = self.list_subnets(identifier=identifier, mask='id') + return [result['id'] for result in results] + def _get_vlans(self, **kwargs): """ Returns a list of VLANs. @@ -107,6 +172,20 @@ def _get_vlans(self, **kwargs): return self.account.getNetworkVlans(mask=self._get_vlan_mask(), **kwargs) + @staticmethod + def _get_subnet_mask(): + """ Returns the standard subnet object mask. + + Wrapper method to prevent duplicated code. + + """ + return [ + 'hardware', + 'datacenter', + 'ipAddressCount', + 'virtualGuests', + ] + @staticmethod def _get_vlan_mask(): """ Returns the standard VLAN object mask. From 5190fa1f792542e2a277e83d9656b4aafb71c39d Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 6 Aug 2013 11:11:01 -0500 Subject: [PATCH 049/168] Adding in subnet unit tests --- SoftLayer/tests/managers/network_tests.py | 71 ++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 26c571a0d..88c0846ca 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -7,7 +7,6 @@ """ from SoftLayer import NetworkManager from SoftLayer.tests import unittest - from mock import MagicMock, ANY, call @@ -17,6 +16,14 @@ def setUp(self): self.client = MagicMock() self.network = NetworkManager(self.client) + def test_get_subnet(self): + id = 9876 + mcall = call(id=id, mask=ANY) + service = self.client['Network_Subnet'] + + self.network.get_subnet(id) + service.getObject.assert_has_calls(mcall) + def test_get_vlan(self): id = 1234 mcall = call(id=id, mask=ANY) @@ -25,6 +32,45 @@ def test_get_vlan(self): self.network.get_vlan(id) service.getObject.assert_has_calls(mcall) + def test_list_subnets_default(self): + mcall = call(filter={}, mask=ANY) + service = self.client['Account'] + + self.network.list_subnets() + + service.getSubnets.assert_has_calls(mcall) + + def test_list_subnets_with_filters(self): + identifier = '10.0.0.1' + datacenter = 'dal00' + version = 4 + + service = self.client['Account'] + service.getSubnets.return_value = [ + { + 'id': 100, + 'networkIdentifier': '10.0.0.1', + 'datacenter': {'name': 'dal00'}, + 'version': 4, + }, + { + 'id': 101, + 'networkIdentifier': '10.0.1.1', + 'datacenter': {'name': 'dal05'}, + 'version': 4, + }, + ] + + result = self.network.list_subnets( + identifier=identifier, + datacenter=datacenter, + version=version, + ) + + service.getSubnets.assert_called() + + self.assertEqual([service.getSubnets.return_value[0]], result) + def test_list_vlans_default(self): mcall = call(filter={}, mask=ANY) service = self.client['Account'] @@ -86,3 +132,26 @@ def test_summary_by_datacenter(self): service.getNetworkVlans.assert_has_calls(mcall) self.assertEqual(expected, result) + + def test_resolve_ids_ip(self): + service = self.client['Account'] + service.getSubnets.return_value = [ + { + 'id': '100', + 'networkIdentifier': '10.0.0.1', + 'datacenter': {'name': 'dal00'}, + 'version': 4, + }, + { + 'id': '101', + 'networkIdentifier': '10.0.1.1', + 'datacenter': {'name': 'dal05'}, + 'version': 4, + }, + ] + + _id = self.network._get_subnet_by_identifier('10.0.0.1') + self.assertEqual(_id, ['100']) + + _id = self.network._get_subnet_by_identifier('nope') + self.assertEqual(_id, []) From b1ee1cb56f29d4b54f8affa5a4c9cf6875985a21 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 6 Aug 2013 14:13:34 -0500 Subject: [PATCH 050/168] Adding IP lookup call to the manager and the CLI --- SoftLayer/CLI/modules/network.py | 73 +++++++++++++++++++---- SoftLayer/managers/network.py | 15 +++++ SoftLayer/tests/managers/network_tests.py | 8 +++ 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index 613127097..f290853b4 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -4,6 +4,7 @@ Perform various network operations The available commands are: + ip-lookup Find information about a specific IP summary Provide a summary view of the network subnet-detail Display detailed information about a subnet subnet-list Show a list of all subnets on the network @@ -13,15 +14,63 @@ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. -from os import linesep -import os.path - from SoftLayer import NetworkManager -from SoftLayer.CLI import ( - CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, - FormattedItem) -from SoftLayer.CLI.helpers import ( - CLIAbort, ArgumentError, SequentialOutput, NestedDict, blank, resolve_id) +from SoftLayer.CLI import (CLIRunnable, Table, KeyValueTable) + + +class NetworkFindIp(CLIRunnable): + """ +usage: sl network ip-lookup + +Finds an IP address on the network and displays its subnet and VLAN +information. + +""" + action = 'ip-lookup' + + @staticmethod + def execute(client, args): + mgr = NetworkManager(client) + + ip = mgr.ip_lookup(args['']) + + if not ip: + return 'Not found' + + t = KeyValueTable(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + + t.add_row(['id', ip['id']]) + t.add_row(['ip', ip['ipAddress']]) + + subnet_table = KeyValueTable(['Name', 'Value']) + subnet_table.align['Name'] = 'r' + subnet_table.align['Value'] = 'l' + subnet_table.add_row(['id', ip['subnet']['id']]) + subnet_table.add_row(['identifier', ip['subnet']['networkIdentifier']]) + subnet_table.add_row(['netmask', ip['subnet']['netmask']]) + if ip['subnet'].get('gateway'): + subnet_table.add_row(['gateway', ip['subnet']['gateway']]) + subnet_table.add_row(['type', ip['subnet']['subnetType']]) + + t.add_row(['subnet', subnet_table]) + + if ip.get('virtualGuest') or ip.get('hardware'): + device_table = KeyValueTable(['Name', 'Value']) + device_table.align['Name'] = 'r' + device_table.align['Value'] = 'l' + if ip.get('virtualGuest'): + device = ip['virtualGuest'] + device_type = 'cci' + else: + device = ip['hardware'] + device_type = 'server' + device_table.add_row(['id', device['id']]) + device_table.add_row(['name', device['fullyQualifiedDomainName']]) + device_table.add_row(['type', device_type]) + t.add_row(['device', device_table]) + return t class NetworkSummary(CLIRunnable): @@ -79,7 +128,7 @@ def execute(client, args): subnet = mgr.get_subnet(args.get('')) - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' @@ -190,7 +239,7 @@ def execute(client, args): vlan = mgr.get_vlan(args.get('')) - t = Table(['Name', 'Value']) + t = KeyValueTable(['Name', 'Value']) t.align['Name'] = 'r' t.align['Value'] = 'l' @@ -203,7 +252,7 @@ def execute(client, args): t.add_row(['firewall', 'Yes' if vlan['firewallInterfaces'] else 'No']) subnets = [] for subnet in vlan['subnets']: - subnet_table = Table(['Name', 'Value']) + subnet_table = KeyValueTable(['Name', 'Value']) subnet_table.align['Name'] = 'r' subnet_table.align['Value'] = 'l' subnet_table.add_row(['id', subnet['id']]) @@ -219,7 +268,7 @@ def execute(client, args): if not args.get('--no-cci'): if vlan['virtualGuests']: - cci_table = Table(['Hostname', 'Domain', 'IP']) + cci_table = KeyValueTable(['Hostname', 'Domain', 'IP']) cci_table.align['Hostname'] = 'r' cci_table.align['IP'] = 'l' for cci in vlan['virtualGuests']: diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index da61acde2..1541aad98 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -24,6 +24,21 @@ def __init__(self, client): self.subnet = client['Network_Subnet'] self.subnet_resolvers = [self._get_subnet_by_identifier] + def ip_lookup(self, ip): + """ Looks up an IP address and returns network information about it. + + :param string ip: An IP address. Can be IPv4 or IPv6 + :returns: A dictionary of information about the IP + + """ + mask = [ + 'hardware', + 'virtualGuest' + ] + mask = 'mask[%s]' % ','.join(mask) + obj = self.client['Network_Subnet_IpAddress'] + return obj.getByIpAddress(ip, mask=mask) + def get_vlan(self, id): """ Returns information about a single VLAN. diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 88c0846ca..acb074be3 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -16,6 +16,14 @@ def setUp(self): self.client = MagicMock() self.network = NetworkManager(self.client) + def test_ip_lookup(self): + ip = '10.0.1.37' + mcall = call(ip, mask=ANY) + service = self.client['Network_Subnet_IpAddress'] + + self.network.ip_lookup(ip) + service.getByIpAddress.assert_has_calls(mcall) + def test_get_subnet(self): id = 9876 mcall = call(id=id, mask=ANY) From 7d7a24d95efcc3660108bface7999e8272bccd56 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 7 Aug 2013 09:55:31 -0500 Subject: [PATCH 051/168] Subnet ordering works for most things. Waiting on an email response in order to get IPV6 working --- SoftLayer/CLI/modules/network.py | 83 +++++++++++++++++++++++++++++++- SoftLayer/managers/network.py | 63 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index f290853b4..e049ec582 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -6,6 +6,7 @@ The available commands are: ip-lookup Find information about a specific IP summary Provide a summary view of the network + subnet-add Create a new subnet subnet-detail Display detailed information about a subnet subnet-list Show a list of all subnets on the network vlan-detail Display detailed information about a VLAN @@ -15,7 +16,9 @@ # :license: BSD, see LICENSE for more details. from SoftLayer import NetworkManager -from SoftLayer.CLI import (CLIRunnable, Table, KeyValueTable) +from SoftLayer.CLI import (CLIRunnable, Table, KeyValueTable, FormattedItem, + confirm) +from SoftLayer.CLI.helpers import (CLIAbort, SequentialOutput) class NetworkFindIp(CLIRunnable): @@ -110,6 +113,84 @@ def execute(client, args): return t +class SubnetAdd(CLIRunnable): + """ +usage: + sl network subnet-add (public|private) [options] + sl network subnet-add global [options] + +Add a new subnet to your account + +Required: + The number of IPs to include in the subnet. + Valid quantities vary by type. + + Type - Valid Quantities (IPv4) + global - 1 + public - 4, 8, 16, 32 + private - 4, 8, 16, 32, 64 + + Type - Valid Quantities (IPv6) + global - 1 + public - 64 + The VLAN ID you want to attach this subnet to + +Options: + --v6 Orders IPv6 + --dry-run, --test Do not order the subnet; just get a quote +""" + action = 'subnet-add' + options = ['confirm'] + + @staticmethod + def execute(client, args): + mgr = NetworkManager(client) + + _type = 'private' + if args['public']: + _type = 'public' + elif args['global']: + _type = 'global' + + version = 4 + if args.get('--v6'): + version = 6 + if not args.get('--test') and not args['--really']: + if not confirm("This action will incur charges on your account." + "Continue?"): + raise CLIAbort('Cancelling order.') + result = mgr.add_subnet(type=_type, + quantity=args[''], + vlan_id=args[''], + version=version, + test_order=args.get('--test')) + if not result: + return 'Unable to place order: No valid price IDs found.' + t = Table(['Item', 'cost']) + t.align['Item'] = 'r' + t.align['cost'] = 'r' + + total = 0.0 + for price in result['prices']: + total += float(price.get('recurringFee', 0.0)) + if args.get('--hourly'): + rate = "%.2f" % float(price['hourlyRecurringFee']) + else: + rate = "%.2f" % float(price['recurringFee']) + + t.add_row([price['item']['description'], rate]) + + t.add_row(['Total monthly cost', "%.2f" % total]) + output = SequentialOutput() + output.append(t) + output.append(FormattedItem( + '', + ' -- ! Prices reflected here are retail and do not ' + 'take account level discounts and are not guarenteed.') + ) + return t + + class SubnetDetail(CLIRunnable): """ usage: sl network subnet-detail [options] diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 1541aad98..461abc4d9 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -24,6 +24,69 @@ def __init__(self, client): self.subnet = client['Network_Subnet'] self.subnet_resolvers = [self._get_subnet_by_identifier] + def add_subnet(self, type, quantity=None, vlan_id=None, version=4, + test_order=False): + package = self.client['Product_Package'] +# ip_categories = [ +# 'global_ipv4', +# 'global_ipv6', +# 'sov_sec_ip_addresses_priv', +# 'sov_sec_ip_addresses_pub', +# 'static_ipv6_addresses', +# 'static_sec_ip_addresses', +# ] + category = 'sov_sec_ip_addresses_priv' + if version == 4: + if type == 'global': + quantity = 0 + category = 'global_ipv4' + elif type == 'public': + category = 'sov_sec_ip_addresses_pub' + else: + category = 'static_ipv6_addresses' + if type == 'global': + quantity = 0 + category = 'global_ipv6' + desc = 'Global' + elif type == 'public': + desc = 'Portable' + + # Filters don't appear to work for Product_Package either +# _filter = {'itemCategory': {}} +# _filter['itemCategory']['categoryCode'] = { +# 'operation': 'in', +# 'options': [{'name': 'data', 'value': ip_categories}], +# } + price_id = None + quantity = str(quantity) + for item in package.getItems(id=0, mask='mask[itemCategory]'): + category_code = item.get('itemCategory', {}).get('categoryCode') + if category_code == category and item['capacity'] == quantity: + if version == 4 or (version == 6 + and desc in item['description']): + price_id = item['prices'][0]['id'] + + order = { + 'packageId': 0, + 'prices': [{'id': price_id}], + 'quantity': 1, + } + + if type != 'global': + order['endPointVlanId'] = vlan_id + vlan = self.get_vlan(vlan_id) + order['location'] = vlan['primaryRouter']['datacenter']['id'] + + if not price_id: + return None + + func = 'placeOrder' + if test_order: + func = 'verifyOrder' + func = getattr(self.client['Product_Order'], func) + + return func(order) + def ip_lookup(self, ip): """ Looks up an IP address and returns network information about it. From d6138772ede326cb955e687f0525404d01572f55 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 7 Aug 2013 11:17:41 -0500 Subject: [PATCH 052/168] Adding in unit tests for add_subnet. Also adjusting some CLI and manager calls based upon API discovery --- SoftLayer/CLI/modules/network.py | 5 +- SoftLayer/managers/network.py | 4 +- SoftLayer/tests/managers/network_tests.py | 184 ++++++++++++++++++++++ 3 files changed, 187 insertions(+), 6 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index e049ec582..b58c2aad0 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -173,10 +173,7 @@ def execute(client, args): total = 0.0 for price in result['prices']: total += float(price.get('recurringFee', 0.0)) - if args.get('--hourly'): - rate = "%.2f" % float(price['hourlyRecurringFee']) - else: - rate = "%.2f" % float(price['recurringFee']) + rate = "%.2f" % float(price['recurringFee']) t.add_row([price['item']['description'], rate]) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 461abc4d9..2ede18c07 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -74,8 +74,8 @@ def add_subnet(self, type, quantity=None, vlan_id=None, version=4, if type != 'global': order['endPointVlanId'] = vlan_id - vlan = self.get_vlan(vlan_id) - order['location'] = vlan['primaryRouter']['datacenter']['id'] +# vlan = self.get_vlan(vlan_id) +# order['location'] = vlan['primaryRouter']['datacenter']['id'] if not price_id: return None diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index acb074be3..133b260a7 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -24,6 +24,91 @@ def test_ip_lookup(self): self.network.ip_lookup(ip) service.getByIpAddress.assert_has_calls(mcall) + def test_add_subnet_for_ipv4(self): + self._setup_add_subnet_mocks() + + # Test a four public address IPv4 order + expected = {'location': 12340, + 'locationObject': {'id': 12340, + 'longName': 'Test Data Center', + 'name': 'test00'}, + 'packageId': 0, + 'prices': [{ + 'categories': [{ + 'categoryCode': 'sov_sec_ip_addresses_pub'}], + 'id': 4444, + 'item': { + 'capacity': '4', + 'description': '4 Portable Public IP Addresses', + 'id': 4440}, + 'itemId': 4440, + 'recurringFee': '0'}]} + + result = self.network.add_subnet(type='public', + quantity=4, + vlan_id=1234, + version=4, + test_order=True) + + # Test a global IPv4 order + expected = {'packageId': 0, + 'prices': [{ + 'categories': [{ + 'categoryCode': 'global_ipv4'}], + 'id': 11, + 'item': {'capacity': '0', + 'description': 'Global IPv4', + 'id': 10}, + 'itemId': 10, + 'recurringFee': '0'}]} + + result = self.network.add_subnet(type='global', + test_order=True) + + def test_add_subnet_for_ipv6(self): + self._setup_add_subnet_mocks() + + # Test a public IPv6 order + expected = { + 'location': 456780, + 'locationObject': {'id': 456780, + 'longName': 'Test Data Center', + 'name': 'test00'}, + 'packageId': 0, + 'prices': [{ + 'categories': [{'categoryCode': 'static_ipv6_addresses'}], + 'id': 664641, + 'item': { + 'capacity': '64', + 'description': '/64 Block Portable Public IPv6 Addresses', + 'id': 66464}, + 'itemId': 66464, + 'recurringFee': '0'}]} + + result = self.network.add_subnet(type='public', + quantity=64, + vlan_id=45678, + version=6, + test_order=True) + + # Test a global IPv6 order + expected = {'packageId': 0, + 'prices': [{ + 'categories': [{ + 'categoryCode': 'global_ipv6'}], + 'id': 611, + 'item': {'capacity': '0', + 'description': 'Global IPv6', + 'id': 610}, + 'itemId': 610, + 'recurringFee': '0'}]} + + result = self.network.add_subnet(type='global', + version=6, + test_order=True) + + self.assertEqual(expected, result) + def test_get_subnet(self): id = 9876 mcall = call(id=id, mask=ANY) @@ -163,3 +248,102 @@ def test_resolve_ids_ip(self): _id = self.network._get_subnet_by_identifier('nope') self.assertEqual(_id, []) + + def _setup_add_subnet_mocks(self): + package_mock = self.client['Product_Package'] + package_mock.getItems.return_value = [ + { + 'id': 4440, + 'capacity': '4', + 'description': '4 Portable Public IP Addresses', + 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_pub'}, + 'prices': [{'id': 4444}], + }, + { + 'id': 8880, + 'capacity': '8', + 'description': '8 Portable Public IP Addresses', + 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_pub'}, + 'prices': [{'id': 8888}], + }, + { + 'id': 44400, + 'capacity': '4', + 'description': '4 Portable Private IP Addresses', + 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_priv'}, + 'prices': [{'id': 44441}], + }, + { + 'id': 88800, + 'capacity': '8', + 'description': '8 Portable Private IP Addresses', + 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_priv'}, + 'prices': [{'id': 88881}], + }, + { + 'id': 10, + 'capacity': '0', + 'description': 'Global IPv4', + 'itemCategory': {'categoryCode': 'global_ipv4'}, + 'prices': [{'id': 11}], + }, + { + 'id': 66464, + 'capacity': '64', + 'description': '/64 Block Portable Public IPv6 Addresses', + 'itemCategory': {'categoryCode': 'static_ipv6_addresses'}, + 'prices': [{'id': 664641}], + }, + { + 'id': 610, + 'capacity': '0', + 'description': 'Global IPv6', + 'itemCategory': {'categoryCode': 'global_ipv6'}, + 'prices': [{'id': 611}], + }, + ] + + def vlan_return_mock(id, mask): + return {'primaryRouter': {'datacenter': {'id': id * 10}}} + + vlan_mock = self.client['Network_Vlan'] + vlan_mock.getObject.side_effect = vlan_return_mock + + def order_return_mock(order): + mock_item = {} + for item in package_mock.getItems.return_value: + if item['prices'][0]['id'] == order['prices'][0]['id']: + mock_item = item + + result = { + 'packageId': 0, + 'prices': [ + { + 'itemId': mock_item['id'], + 'recurringFee': '0', + 'id': mock_item['prices'][0]['id'], + 'item': { + 'capacity': mock_item['capacity'], + 'description': mock_item['description'], + 'id': mock_item['id'] + }, + 'categories': [{ + 'categoryCode': + mock_item['itemCategory']['categoryCode'] + }], + } + ], + } + + if order.get('location'): + result['locationObject'] = { + 'id': order['location'], + 'name': 'test00', + 'longName': 'Test Data Center' + } + result['location'] = order['location'] + + return result + + order_mock = self.client['Product_Order'] + order_mock.verifyOrder.side_effect = order_return_mock From d70f99ce567091661147ca311aa98a1b450e120a Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 7 Aug 2013 15:29:47 -0500 Subject: [PATCH 053/168] Adding missing functionality test --- SoftLayer/tests/managers/network_tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 133b260a7..148a7122d 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -24,6 +24,11 @@ def test_ip_lookup(self): self.network.ip_lookup(ip) service.getByIpAddress.assert_has_calls(mcall) + def test_add_subnet_returns_none_on_failure(self): + self._setup_add_subnet_mocks() + + self.assertEqual(None, self.network.add_subnet(type='bad')) + def test_add_subnet_for_ipv4(self): self._setup_add_subnet_mocks() From eaa55d60fdeb246b4787c603a69bcc87fb1a5eb4 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 9 Aug 2013 08:26:23 -0500 Subject: [PATCH 054/168] Removing code that's ultimately unneeded --- SoftLayer/managers/network.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 2ede18c07..2d7985958 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -74,8 +74,6 @@ def add_subnet(self, type, quantity=None, vlan_id=None, version=4, if type != 'global': order['endPointVlanId'] = vlan_id -# vlan = self.get_vlan(vlan_id) -# order['location'] = vlan['primaryRouter']['datacenter']['id'] if not price_id: return None From 2e14c0b1efa8723731f507da9a9a396144a05d0d Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 9 Aug 2013 14:10:46 -0500 Subject: [PATCH 055/168] Commenting out subnet ordering due to an API bug --- SoftLayer/CLI/modules/network.py | 150 ++++++++++++++++--------------- SoftLayer/managers/network.py | 107 ++++++++++------------ 2 files changed, 123 insertions(+), 134 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index b58c2aad0..575fd50fe 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -6,12 +6,13 @@ The available commands are: ip-lookup Find information about a specific IP summary Provide a summary view of the network - subnet-add Create a new subnet subnet-detail Display detailed information about a subnet subnet-list Show a list of all subnets on the network vlan-detail Display detailed information about a VLAN vlan-list Show a list of all VLANs on the network """ +# Removed the following line due to an API bug: +# subnet-add Create a new subnet # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. @@ -113,79 +114,80 @@ def execute(client, args): return t -class SubnetAdd(CLIRunnable): - """ -usage: - sl network subnet-add (public|private) [options] - sl network subnet-add global [options] - -Add a new subnet to your account - -Required: - The number of IPs to include in the subnet. - Valid quantities vary by type. - - Type - Valid Quantities (IPv4) - global - 1 - public - 4, 8, 16, 32 - private - 4, 8, 16, 32, 64 - - Type - Valid Quantities (IPv6) - global - 1 - public - 64 - The VLAN ID you want to attach this subnet to - -Options: - --v6 Orders IPv6 - --dry-run, --test Do not order the subnet; just get a quote -""" - action = 'subnet-add' - options = ['confirm'] - - @staticmethod - def execute(client, args): - mgr = NetworkManager(client) - - _type = 'private' - if args['public']: - _type = 'public' - elif args['global']: - _type = 'global' - - version = 4 - if args.get('--v6'): - version = 6 - if not args.get('--test') and not args['--really']: - if not confirm("This action will incur charges on your account." - "Continue?"): - raise CLIAbort('Cancelling order.') - result = mgr.add_subnet(type=_type, - quantity=args[''], - vlan_id=args[''], - version=version, - test_order=args.get('--test')) - if not result: - return 'Unable to place order: No valid price IDs found.' - t = Table(['Item', 'cost']) - t.align['Item'] = 'r' - t.align['cost'] = 'r' - - total = 0.0 - for price in result['prices']: - total += float(price.get('recurringFee', 0.0)) - rate = "%.2f" % float(price['recurringFee']) - - t.add_row([price['item']['description'], rate]) - - t.add_row(['Total monthly cost', "%.2f" % total]) - output = SequentialOutput() - output.append(t) - output.append(FormattedItem( - '', - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guarenteed.') - ) - return t +# Temporarily removing this due to an API bug not allowing subnet ordering +# class SubnetAdd(CLIRunnable): +# """ +# usage: +# sl network subnet-add (public|private) [options] +# sl network subnet-add global [options] + +# Add a new subnet to your account + +# Required: +# The number of IPs to include in the subnet. +# Valid quantities vary by type. + +# Type - Valid Quantities (IPv4) +# global - 1 +# public - 4, 8, 16, 32 +# private - 4, 8, 16, 32, 64 + +# Type - Valid Quantities (IPv6) +# global - 1 +# public - 64 +# The VLAN ID you want to attach this subnet to + +# Options: +# --v6 Orders IPv6 +# --dry-run, --test Do not order the subnet; just get a quote +# """ +# action = 'subnet-add' +# options = ['confirm'] + +# @staticmethod +# def execute(client, args): +# mgr = NetworkManager(client) + +# _type = 'private' +# if args['public']: +# _type = 'public' +# elif args['global']: +# _type = 'global' + +# version = 4 +# if args.get('--v6'): +# version = 6 +# if not args.get('--test') and not args['--really']: +# if not confirm("This action will incur charges on your account." +# "Continue?"): +# raise CLIAbort('Cancelling order.') +# result = mgr.add_subnet(type=_type, +# quantity=args[''], +# vlan_id=args[''], +# version=version, +# test_order=args.get('--test')) +# if not result: +# return 'Unable to place order: No valid price IDs found.' +# t = Table(['Item', 'cost']) +# t.align['Item'] = 'r' +# t.align['cost'] = 'r' + +# total = 0.0 +# for price in result['prices']: +# total += float(price.get('recurringFee', 0.0)) +# rate = "%.2f" % float(price['recurringFee']) + +# t.add_row([price['item']['description'], rate]) + +# t.add_row(['Total monthly cost', "%.2f" % total]) +# output = SequentialOutput() +# output.append(t) +# output.append(FormattedItem( +# '', +# ' -- ! Prices reflected here are retail and do not ' +# 'take account level discounts and are not guarenteed.') +# ) +# return t class SubnetDetail(CLIRunnable): diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 2d7985958..0613d554d 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -24,66 +24,53 @@ def __init__(self, client): self.subnet = client['Network_Subnet'] self.subnet_resolvers = [self._get_subnet_by_identifier] - def add_subnet(self, type, quantity=None, vlan_id=None, version=4, - test_order=False): - package = self.client['Product_Package'] -# ip_categories = [ -# 'global_ipv4', -# 'global_ipv6', -# 'sov_sec_ip_addresses_priv', -# 'sov_sec_ip_addresses_pub', -# 'static_ipv6_addresses', -# 'static_sec_ip_addresses', -# ] - category = 'sov_sec_ip_addresses_priv' - if version == 4: - if type == 'global': - quantity = 0 - category = 'global_ipv4' - elif type == 'public': - category = 'sov_sec_ip_addresses_pub' - else: - category = 'static_ipv6_addresses' - if type == 'global': - quantity = 0 - category = 'global_ipv6' - desc = 'Global' - elif type == 'public': - desc = 'Portable' - - # Filters don't appear to work for Product_Package either -# _filter = {'itemCategory': {}} -# _filter['itemCategory']['categoryCode'] = { -# 'operation': 'in', -# 'options': [{'name': 'data', 'value': ip_categories}], -# } - price_id = None - quantity = str(quantity) - for item in package.getItems(id=0, mask='mask[itemCategory]'): - category_code = item.get('itemCategory', {}).get('categoryCode') - if category_code == category and item['capacity'] == quantity: - if version == 4 or (version == 6 - and desc in item['description']): - price_id = item['prices'][0]['id'] - - order = { - 'packageId': 0, - 'prices': [{'id': price_id}], - 'quantity': 1, - } - - if type != 'global': - order['endPointVlanId'] = vlan_id - - if not price_id: - return None - - func = 'placeOrder' - if test_order: - func = 'verifyOrder' - func = getattr(self.client['Product_Order'], func) - - return func(order) + # Temporarily removing due to a bug in the API not allowing subnet ordering + # def add_subnet(self, type, quantity=None, vlan_id=None, version=4, + # test_order=False): + # package = self.client['Product_Package'] + # category = 'sov_sec_ip_addresses_priv' + # if version == 4: + # if type == 'global': + # quantity = 0 + # category = 'global_ipv4' + # elif type == 'public': + # category = 'sov_sec_ip_addresses_pub' + # else: + # category = 'static_ipv6_addresses' + # if type == 'global': + # quantity = 0 + # category = 'global_ipv6' + # desc = 'Global' + # elif type == 'public': + # desc = 'Portable' + + # price_id = None + # quantity = str(quantity) + # for item in package.getItems(id=0, mask='mask[itemCategory]'): + # category_code = item.get('itemCategory', {}).get('categoryCode') + # if category_code == category and item['capacity'] == quantity: + # if version == 4 or (version == 6 + # and desc in item['description']): + # price_id = item['prices'][0]['id'] + + # order = { + # 'packageId': 0, + # 'prices': [{'id': price_id}], + # 'quantity': 1, + # } + + # if type != 'global': + # order['endPointVlanId'] = vlan_id + + # if not price_id: + # return None + + # func = 'placeOrder' + # if test_order: + # func = 'verifyOrder' + # func = getattr(self.client['Product_Order'], func) + + # return func(order) def ip_lookup(self, ip): """ Looks up an IP address and returns network information about it. From 7daa65173e4e0853e13edb08cee9e43b373e8b42 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 9 Aug 2013 14:12:01 -0500 Subject: [PATCH 056/168] Commenting out unit tests too --- SoftLayer/tests/managers/network_tests.py | 180 +++++++++++----------- 1 file changed, 91 insertions(+), 89 deletions(-) diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 148a7122d..8d6f9cf89 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -24,95 +24,97 @@ def test_ip_lookup(self): self.network.ip_lookup(ip) service.getByIpAddress.assert_has_calls(mcall) - def test_add_subnet_returns_none_on_failure(self): - self._setup_add_subnet_mocks() - - self.assertEqual(None, self.network.add_subnet(type='bad')) - - def test_add_subnet_for_ipv4(self): - self._setup_add_subnet_mocks() - - # Test a four public address IPv4 order - expected = {'location': 12340, - 'locationObject': {'id': 12340, - 'longName': 'Test Data Center', - 'name': 'test00'}, - 'packageId': 0, - 'prices': [{ - 'categories': [{ - 'categoryCode': 'sov_sec_ip_addresses_pub'}], - 'id': 4444, - 'item': { - 'capacity': '4', - 'description': '4 Portable Public IP Addresses', - 'id': 4440}, - 'itemId': 4440, - 'recurringFee': '0'}]} - - result = self.network.add_subnet(type='public', - quantity=4, - vlan_id=1234, - version=4, - test_order=True) - - # Test a global IPv4 order - expected = {'packageId': 0, - 'prices': [{ - 'categories': [{ - 'categoryCode': 'global_ipv4'}], - 'id': 11, - 'item': {'capacity': '0', - 'description': 'Global IPv4', - 'id': 10}, - 'itemId': 10, - 'recurringFee': '0'}]} - - result = self.network.add_subnet(type='global', - test_order=True) - - def test_add_subnet_for_ipv6(self): - self._setup_add_subnet_mocks() - - # Test a public IPv6 order - expected = { - 'location': 456780, - 'locationObject': {'id': 456780, - 'longName': 'Test Data Center', - 'name': 'test00'}, - 'packageId': 0, - 'prices': [{ - 'categories': [{'categoryCode': 'static_ipv6_addresses'}], - 'id': 664641, - 'item': { - 'capacity': '64', - 'description': '/64 Block Portable Public IPv6 Addresses', - 'id': 66464}, - 'itemId': 66464, - 'recurringFee': '0'}]} - - result = self.network.add_subnet(type='public', - quantity=64, - vlan_id=45678, - version=6, - test_order=True) - - # Test a global IPv6 order - expected = {'packageId': 0, - 'prices': [{ - 'categories': [{ - 'categoryCode': 'global_ipv6'}], - 'id': 611, - 'item': {'capacity': '0', - 'description': 'Global IPv6', - 'id': 610}, - 'itemId': 610, - 'recurringFee': '0'}]} - - result = self.network.add_subnet(type='global', - version=6, - test_order=True) - - self.assertEqual(expected, result) + # Temporarily commenting this out due to a bug in the API that prevents + # subnet ordering + # def test_add_subnet_returns_none_on_failure(self): + # self._setup_add_subnet_mocks() + + # self.assertEqual(None, self.network.add_subnet(type='bad')) + + # def test_add_subnet_for_ipv4(self): + # self._setup_add_subnet_mocks() + + # # Test a four public address IPv4 order + # expected = {'location': 12340, + # 'locationObject': {'id': 12340, + # 'longName': 'Test Data Center', + # 'name': 'test00'}, + # 'packageId': 0, + # 'prices': [{ + # 'categories': [{ + # 'categoryCode': 'sov_sec_ip_addresses_pub'}], + # 'id': 4444, + # 'item': { + # 'capacity': '4', + # 'description': '4 Portable Public IP Addresses', + # 'id': 4440}, + # 'itemId': 4440, + # 'recurringFee': '0'}]} + + # result = self.network.add_subnet(type='public', + # quantity=4, + # vlan_id=1234, + # version=4, + # test_order=True) + + # # Test a global IPv4 order + # expected = {'packageId': 0, + # 'prices': [{ + # 'categories': [{ + # 'categoryCode': 'global_ipv4'}], + # 'id': 11, + # 'item': {'capacity': '0', + # 'description': 'Global IPv4', + # 'id': 10}, + # 'itemId': 10, + # 'recurringFee': '0'}]} + + # result = self.network.add_subnet(type='global', + # test_order=True) + + # def test_add_subnet_for_ipv6(self): + # self._setup_add_subnet_mocks() + + # # Test a public IPv6 order + # expected = { + # 'location': 456780, + # 'locationObject': {'id': 456780, + # 'longName': 'Test Data Center', + # 'name': 'test00'}, + # 'packageId': 0, + # 'prices': [{ + # 'categories': [{'categoryCode': 'static_ipv6_addresses'}], + # 'id': 664641, + # 'item': { + # 'capacity': '64', + # 'description': '/64 Block Portable Public IPv6 Addresses', + # 'id': 66464}, + # 'itemId': 66464, + # 'recurringFee': '0'}]} + + # result = self.network.add_subnet(type='public', + # quantity=64, + # vlan_id=45678, + # version=6, + # test_order=True) + + # # Test a global IPv6 order + # expected = {'packageId': 0, + # 'prices': [{ + # 'categories': [{ + # 'categoryCode': 'global_ipv6'}], + # 'id': 611, + # 'item': {'capacity': '0', + # 'description': 'Global IPv6', + # 'id': 610}, + # 'itemId': 610, + # 'recurringFee': '0'}]} + + # result = self.network.add_subnet(type='global', + # version=6, + # test_order=True) + + # self.assertEqual(expected, result) def test_get_subnet(self): id = 9876 From 623feee6ceda5a5ee49a5543910c68992128843c Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 9 Aug 2013 14:49:28 -0500 Subject: [PATCH 057/168] Removing commented out code now that I've backed it up to another branch --- SoftLayer/CLI/modules/network.py | 76 ------------------- SoftLayer/managers/network.py | 48 ------------ SoftLayer/tests/managers/network_tests.py | 92 ----------------------- 3 files changed, 216 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index 575fd50fe..b44a53e7e 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -114,82 +114,6 @@ def execute(client, args): return t -# Temporarily removing this due to an API bug not allowing subnet ordering -# class SubnetAdd(CLIRunnable): -# """ -# usage: -# sl network subnet-add (public|private) [options] -# sl network subnet-add global [options] - -# Add a new subnet to your account - -# Required: -# The number of IPs to include in the subnet. -# Valid quantities vary by type. - -# Type - Valid Quantities (IPv4) -# global - 1 -# public - 4, 8, 16, 32 -# private - 4, 8, 16, 32, 64 - -# Type - Valid Quantities (IPv6) -# global - 1 -# public - 64 -# The VLAN ID you want to attach this subnet to - -# Options: -# --v6 Orders IPv6 -# --dry-run, --test Do not order the subnet; just get a quote -# """ -# action = 'subnet-add' -# options = ['confirm'] - -# @staticmethod -# def execute(client, args): -# mgr = NetworkManager(client) - -# _type = 'private' -# if args['public']: -# _type = 'public' -# elif args['global']: -# _type = 'global' - -# version = 4 -# if args.get('--v6'): -# version = 6 -# if not args.get('--test') and not args['--really']: -# if not confirm("This action will incur charges on your account." -# "Continue?"): -# raise CLIAbort('Cancelling order.') -# result = mgr.add_subnet(type=_type, -# quantity=args[''], -# vlan_id=args[''], -# version=version, -# test_order=args.get('--test')) -# if not result: -# return 'Unable to place order: No valid price IDs found.' -# t = Table(['Item', 'cost']) -# t.align['Item'] = 'r' -# t.align['cost'] = 'r' - -# total = 0.0 -# for price in result['prices']: -# total += float(price.get('recurringFee', 0.0)) -# rate = "%.2f" % float(price['recurringFee']) - -# t.add_row([price['item']['description'], rate]) - -# t.add_row(['Total monthly cost', "%.2f" % total]) -# output = SequentialOutput() -# output.append(t) -# output.append(FormattedItem( -# '', -# ' -- ! Prices reflected here are retail and do not ' -# 'take account level discounts and are not guarenteed.') -# ) -# return t - - class SubnetDetail(CLIRunnable): """ usage: sl network subnet-detail [options] diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 0613d554d..1541aad98 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -24,54 +24,6 @@ def __init__(self, client): self.subnet = client['Network_Subnet'] self.subnet_resolvers = [self._get_subnet_by_identifier] - # Temporarily removing due to a bug in the API not allowing subnet ordering - # def add_subnet(self, type, quantity=None, vlan_id=None, version=4, - # test_order=False): - # package = self.client['Product_Package'] - # category = 'sov_sec_ip_addresses_priv' - # if version == 4: - # if type == 'global': - # quantity = 0 - # category = 'global_ipv4' - # elif type == 'public': - # category = 'sov_sec_ip_addresses_pub' - # else: - # category = 'static_ipv6_addresses' - # if type == 'global': - # quantity = 0 - # category = 'global_ipv6' - # desc = 'Global' - # elif type == 'public': - # desc = 'Portable' - - # price_id = None - # quantity = str(quantity) - # for item in package.getItems(id=0, mask='mask[itemCategory]'): - # category_code = item.get('itemCategory', {}).get('categoryCode') - # if category_code == category and item['capacity'] == quantity: - # if version == 4 or (version == 6 - # and desc in item['description']): - # price_id = item['prices'][0]['id'] - - # order = { - # 'packageId': 0, - # 'prices': [{'id': price_id}], - # 'quantity': 1, - # } - - # if type != 'global': - # order['endPointVlanId'] = vlan_id - - # if not price_id: - # return None - - # func = 'placeOrder' - # if test_order: - # func = 'verifyOrder' - # func = getattr(self.client['Product_Order'], func) - - # return func(order) - def ip_lookup(self, ip): """ Looks up an IP address and returns network information about it. diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 8d6f9cf89..444d49fe0 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -24,98 +24,6 @@ def test_ip_lookup(self): self.network.ip_lookup(ip) service.getByIpAddress.assert_has_calls(mcall) - # Temporarily commenting this out due to a bug in the API that prevents - # subnet ordering - # def test_add_subnet_returns_none_on_failure(self): - # self._setup_add_subnet_mocks() - - # self.assertEqual(None, self.network.add_subnet(type='bad')) - - # def test_add_subnet_for_ipv4(self): - # self._setup_add_subnet_mocks() - - # # Test a four public address IPv4 order - # expected = {'location': 12340, - # 'locationObject': {'id': 12340, - # 'longName': 'Test Data Center', - # 'name': 'test00'}, - # 'packageId': 0, - # 'prices': [{ - # 'categories': [{ - # 'categoryCode': 'sov_sec_ip_addresses_pub'}], - # 'id': 4444, - # 'item': { - # 'capacity': '4', - # 'description': '4 Portable Public IP Addresses', - # 'id': 4440}, - # 'itemId': 4440, - # 'recurringFee': '0'}]} - - # result = self.network.add_subnet(type='public', - # quantity=4, - # vlan_id=1234, - # version=4, - # test_order=True) - - # # Test a global IPv4 order - # expected = {'packageId': 0, - # 'prices': [{ - # 'categories': [{ - # 'categoryCode': 'global_ipv4'}], - # 'id': 11, - # 'item': {'capacity': '0', - # 'description': 'Global IPv4', - # 'id': 10}, - # 'itemId': 10, - # 'recurringFee': '0'}]} - - # result = self.network.add_subnet(type='global', - # test_order=True) - - # def test_add_subnet_for_ipv6(self): - # self._setup_add_subnet_mocks() - - # # Test a public IPv6 order - # expected = { - # 'location': 456780, - # 'locationObject': {'id': 456780, - # 'longName': 'Test Data Center', - # 'name': 'test00'}, - # 'packageId': 0, - # 'prices': [{ - # 'categories': [{'categoryCode': 'static_ipv6_addresses'}], - # 'id': 664641, - # 'item': { - # 'capacity': '64', - # 'description': '/64 Block Portable Public IPv6 Addresses', - # 'id': 66464}, - # 'itemId': 66464, - # 'recurringFee': '0'}]} - - # result = self.network.add_subnet(type='public', - # quantity=64, - # vlan_id=45678, - # version=6, - # test_order=True) - - # # Test a global IPv6 order - # expected = {'packageId': 0, - # 'prices': [{ - # 'categories': [{ - # 'categoryCode': 'global_ipv6'}], - # 'id': 611, - # 'item': {'capacity': '0', - # 'description': 'Global IPv6', - # 'id': 610}, - # 'itemId': 610, - # 'recurringFee': '0'}]} - - # result = self.network.add_subnet(type='global', - # version=6, - # test_order=True) - - # self.assertEqual(expected, result) - def test_get_subnet(self): id = 9876 mcall = call(id=id, mask=ANY) From c0d6145a4e07118014acc1f889097193be88d57f Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 9 Aug 2013 16:02:54 -0500 Subject: [PATCH 058/168] Adding support for ordering private network only CCIs. Also renamed previous 'private' option to 'dedicated' for a more accurate description --- SoftLayer/CLI/modules/cci.py | 7 +++++-- SoftLayer/managers/cci.py | 18 +++++++++++------ SoftLayer/tests/managers/cci_tests.py | 29 +++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index a61dd7d1c..eac01a607 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -349,13 +349,15 @@ class CreateCCI(CLIRunnable): Note: Omitting this value defaults to the first available datacenter -n MBPS, --network=MBPS Network port speed in Mbps - --private Allocate a private CCI + --dedicated Allocate a dedicated CCI (non-shared host) --dry-run, --test Do not create CCI, just get a quote -u --userdata=DATA User defined metadata string -F --userfile=FILE Read userdata from file -i --postinstall=URI Post-install script to download (Only HTTPS executes, HTTP leaves file in /root) + --private Forces the CCI to only have access the private + network. --wait=SECONDS Block until CCI is finished provisioning for up to X seconds before returning. """ @@ -380,8 +382,9 @@ def execute(client, args): "cpus": args['--cpu'], "domain": args['--domain'], "hostname": args['--hostname'], - "private": args['--private'], + "dedicated": args['--dedicated'], "local_disk": True, + "private": args['--private'] } try: diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 40819ee53..a02ef7a65 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -218,8 +218,9 @@ def _generate_create_dict( self, cpus=None, memory=None, hourly=True, hostname=None, domain=None, local_disk=True, datacenter=None, os_code=None, image_id=None, - private=False, public_vlan=None, private_vlan=None, - userdata=None, nic_speed=None, disks=None, post_uri=None): + dedicated=False, public_vlan=None, private_vlan=None, + userdata=None, nic_speed=None, disks=None, post_uri=None, + private=False): """ Translates a list of arguments into a dictionary necessary for creating a CCI. @@ -238,9 +239,9 @@ def _generate_create_dict( if image_id is specified. :param int image_id: The ID of the image to load onto the server. Cannot be specified if os_code is specified. - :param bool private: Flag to indicate if this should be housed on a - private or shared host (default). This will incur - a fee on your account. + :param bool dedicated: Flag to indicate if this should be housed on a + dedicated or shared host (default). This will + incur a fee on your account. :param int public_vlan: The ID of the public VLAN on which you want this CCI placed. :param int private_vlan: The ID of the public VLAN on which you want @@ -250,6 +251,8 @@ def _generate_create_dict( :param list disks: A list of disk capacities for this server. :param string post_url: The URI of the post-install script to run after reload + :param bool private: If true, the CCI will be provisioned only with + access to the private network. Defaults to false """ required = [cpus, memory, hostname, domain] @@ -276,8 +279,11 @@ def _generate_create_dict( data["hourlyBillingFlag"] = hourly + if dedicated: + data["dedicatedAccountHostOnlyFlag"] = dedicated + if private: - data["dedicatedAccountHostOnlyFlag"] = private + data['privateNetworkOnlyFlag'] = private if image_id: data["blockDeviceTemplateGroup"] = {"globalIdentifier": image_id} diff --git a/SoftLayer/tests/managers/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py index ecb1c876f..275f4f105 100644 --- a/SoftLayer/tests/managers/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -212,14 +212,14 @@ def test_generate_image_id(self): self.assertEqual(data, assert_data) - def test_generate_private(self): + def test_generate_dedicated(self): data = self.cci._generate_create_dict( cpus=1, memory=1, hostname='test', domain='example.com', os_code="STRING", - private=True, + dedicated=True, ) assert_data = { @@ -350,6 +350,31 @@ def test_generate_network(self): self.assertEqual(data, assert_data) + def test_generate_private_network_only(self): + data = self.cci._generate_create_dict( + cpus=1, + memory=1, + hostname='test', + domain='example.com', + os_code="STRING", + nic_speed=9001, + private=True + ) + + assert_data = { + 'startCpus': 1, + 'maxMemory': 1, + 'hostname': 'test', + 'domain': 'example.com', + 'localDiskFlag': True, + 'operatingSystemReferenceCode': "STRING", + 'privateNetworkOnlyFlag': True, + 'hourlyBillingFlag': True, + 'networkComponents': [{'maxSpeed': 9001}], + } + + self.assertEqual(data, assert_data) + def test_generate_post_uri(self): data = self.cci._generate_create_dict( cpus=1, From fd46cd71973ea575677e0aa88904afe7a091978e Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 12 Aug 2013 09:39:22 -0500 Subject: [PATCH 059/168] Addressing issues discovered during code review and testing --- SoftLayer/CLI/modules/network.py | 18 +++++++++--------- SoftLayer/managers/network.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index b44a53e7e..6506bbc89 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -22,7 +22,7 @@ from SoftLayer.CLI.helpers import (CLIAbort, SequentialOutput) -class NetworkFindIp(CLIRunnable): +class NetworkLookupIp(CLIRunnable): """ usage: sl network ip-lookup @@ -139,10 +139,10 @@ def execute(client, args): t.add_row(['id', subnet['id']]) t.add_row(['identifier', subnet['networkIdentifier']]) t.add_row(['subnet type', subnet['subnetType']]) - t.add_row(['gateway', subnet['gateway']]) - t.add_row(['broadcast', subnet['broadcastAddress']]) + t.add_row(['gateway', subnet.get('gateway', '-')]) + t.add_row(['broadcast', subnet.get('broadcastAddress', '-')]) t.add_row(['datacenter', subnet['datacenter']['name']]) - t.add_row(['usable ips', subnet['usableIpAddressCount']]) + t.add_row(['usable ips', subnet.get('usableIpAddressCount', '-')]) if not args.get('--no-cci'): if subnet['virtualGuests']: @@ -152,7 +152,7 @@ def execute(client, args): for cci in subnet['virtualGuests']: cci_table.add_row([cci['hostname'], cci['domain'], - cci['primaryIpAddress']]) + cci.get('primaryIpAddress')]) t.add_row(['ccis', cci_table]) else: t.add_row(['cci', 'none']) @@ -165,7 +165,7 @@ def execute(client, args): for hw in subnet['hardware']: hw_table.add_row([hw['hostname'], hw['domain'], - hw['primaryIpAddress']]) + hw.get('primaryIpAddress')]) t.add_row(['hardware', hw_table]) else: t.add_row(['hardware', 'none']) @@ -262,7 +262,7 @@ def execute(client, args): subnet_table.add_row(['id', subnet['id']]) subnet_table.add_row(['identifier', subnet['networkIdentifier']]) subnet_table.add_row(['netmask', subnet['netmask']]) - subnet_table.add_row(['gateway', subnet['gateway']]) + subnet_table.add_row(['gateway', subnet.get('gateway', '-')]) subnet_table.add_row(['type', subnet['subnetType']]) subnet_table.add_row(['usable ips', subnet['usableIpAddressCount']]) @@ -278,7 +278,7 @@ def execute(client, args): for cci in vlan['virtualGuests']: cci_table.add_row([cci['hostname'], cci['domain'], - cci['primaryIpAddress']]) + cci.get('primaryIpAddress')]) t.add_row(['ccis', cci_table]) else: t.add_row(['cci', 'none']) @@ -291,7 +291,7 @@ def execute(client, args): for hw in vlan['hardware']: hw_table.add_row([hw['hostname'], hw['domain'], - hw['primaryIpAddress']]) + hw.get('primaryIpAddress')]) t.add_row(['hardware', hw_table]) else: t.add_row(['hardware', 'none']) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 1541aad98..ad70140cf 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -173,7 +173,7 @@ def _get_subnet_by_identifier(self, identifier): :param string identifier: The identifier to look up :returns: The ID of the matching subnet or None """ - results = self.list_subnets(identifier=identifier, mask='id') + results = self.list_subnets(identifier=identifier) return [result['id'] for result in results] def _get_vlans(self, **kwargs): From 77d744bbebb9856ccdcdce0da7c7cfc5ca068ce6 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 12 Aug 2013 10:04:04 -0500 Subject: [PATCH 060/168] Coverting code to use API filters instead of client side --- SoftLayer/CLI/modules/network.py | 2 ++ SoftLayer/managers/network.py | 25 +++++++------------------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/SoftLayer/CLI/modules/network.py b/SoftLayer/CLI/modules/network.py index 6506bbc89..7a954e132 100644 --- a/SoftLayer/CLI/modules/network.py +++ b/SoftLayer/CLI/modules/network.py @@ -187,6 +187,7 @@ class SubnetList(CLIRunnable): -d DC, --datacenter=DC datacenter shortname (sng01, dal05, ...) --v4 Display only IPV4 subnets --v6 Display only IPV6 subnets + --identifier=ID Filter by identifier """ action = 'subnet-list' @@ -209,6 +210,7 @@ def execute(client, args): subnets = mgr.list_subnets( datacenter=args.get('--datacenter'), version=version, + identifier=args.get('--identifier'), ) for subnet in subnets: diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index ad70140cf..bf508ea8a 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -104,28 +104,17 @@ def list_subnets(self, identifier=None, datacenter=None, version=0, _filter = NestedDict(kwargs.get('filter') or {}) - # TODO - I don't think filtering works on subnets in the API - #if identifier: - # _filter['networkIdentifier'] = query_filter(identifier) - #if datacenter: - # _filter['networkVlans']['primaryRouter']['datacenter']['name'] = \ - # query_filter(datacenter) - # if version: - # _filter['version'] = query_filter(version) + if identifier: + _filter['subnets']['networkIdentifier'] = query_filter(identifier) + if datacenter: + _filter['subnets']['datacenter']['name'] = \ + query_filter(datacenter) + if version: + _filter['subnets']['version'] = query_filter(version) kwargs['filter'] = _filter.to_dict() results = self.account.getSubnets(**kwargs) - - if any([version, identifier, datacenter]): - if version: - results = filter(lambda x: x['version'] == version, results) - if identifier: - results = filter(lambda x: x['networkIdentifier'] == - identifier, results) - if datacenter: - results = filter(lambda x: x['datacenter']['name'] == - datacenter, results) return results def summary_by_datacenter(self): From e5cc902f959dd4f3b2a30d50ba2f34d1562f1dd9 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 12 Aug 2013 10:15:53 -0500 Subject: [PATCH 061/168] Fixing unit tests and reintroducing mask since API filtering works --- SoftLayer/managers/network.py | 2 +- SoftLayer/tests/managers/network_tests.py | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index bf508ea8a..1382638db 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -162,7 +162,7 @@ def _get_subnet_by_identifier(self, identifier): :param string identifier: The identifier to look up :returns: The ID of the matching subnet or None """ - results = self.list_subnets(identifier=identifier) + results = self.list_subnets(identifier=identifier, mask='id') return [result['id'] for result in results] def _get_vlans(self, **kwargs): diff --git a/SoftLayer/tests/managers/network_tests.py b/SoftLayer/tests/managers/network_tests.py index 444d49fe0..80a2d2a46 100644 --- a/SoftLayer/tests/managers/network_tests.py +++ b/SoftLayer/tests/managers/network_tests.py @@ -61,12 +61,6 @@ def test_list_subnets_with_filters(self): 'datacenter': {'name': 'dal00'}, 'version': 4, }, - { - 'id': 101, - 'networkIdentifier': '10.0.1.1', - 'datacenter': {'name': 'dal05'}, - 'version': 4, - }, ] result = self.network.list_subnets( @@ -143,20 +137,14 @@ def test_summary_by_datacenter(self): def test_resolve_ids_ip(self): service = self.client['Account'] - service.getSubnets.return_value = [ + service.getSubnets.side_effect = [[ { 'id': '100', 'networkIdentifier': '10.0.0.1', 'datacenter': {'name': 'dal00'}, 'version': 4, }, - { - 'id': '101', - 'networkIdentifier': '10.0.1.1', - 'datacenter': {'name': 'dal05'}, - 'version': 4, - }, - ] + ], []] _id = self.network._get_subnet_by_identifier('10.0.0.1') self.assertEqual(_id, ['100']) From 1ef4fd0095f4e0507bb4d9dfdb7d60b7c187665e Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 12 Aug 2013 11:52:34 -0500 Subject: [PATCH 062/168] Adding SSH key support --- SoftLayer/CLI/core.py | 1 + SoftLayer/CLI/modules/cci.py | 10 +- SoftLayer/CLI/modules/hardware.py | 9 +- SoftLayer/CLI/modules/sshkey.py | 167 +++++++++++++++++++ SoftLayer/managers/__init__.py | 3 +- SoftLayer/managers/cci.py | 16 +- SoftLayer/managers/hardware.py | 6 +- SoftLayer/managers/sshkey.py | 90 ++++++++++ SoftLayer/tests/managers/cci_tests.py | 16 ++ SoftLayer/tests/managers/hardware_tests.py | 182 +++++++++++++++------ SoftLayer/tests/managers/sshkey_tests.py | 90 ++++++++++ 11 files changed, 534 insertions(+), 56 deletions(-) create mode 100644 SoftLayer/CLI/modules/sshkey.py create mode 100644 SoftLayer/managers/sshkey.py create mode 100644 SoftLayer/tests/managers/sshkey_tests.py diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index be8ceaec7..d9724bc3e 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -19,6 +19,7 @@ image Manages compute and flex images metadata Get details about this machine. Also available with 'my' and 'meta' nas View NAS details + sshkey Manage SSH keys on your account ssl Manages SSL See 'sl help ' for more information on a specific module. diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index eac01a607..c4af2b87f 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -26,7 +26,7 @@ from os import linesep import os.path -from SoftLayer import CCIManager +from SoftLayer import CCIManager, SshKeyManager from SoftLayer.CLI import ( CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, FormattedItem) @@ -356,8 +356,10 @@ class CreateCCI(CLIRunnable): -F --userfile=FILE Read userdata from file -i --postinstall=URI Post-install script to download (Only HTTPS executes, HTTP leaves file in /root) + -k KEY, --key=KEY The SSH key to add to the root user --private Forces the CCI to only have access the private network. + -k KEY, --key=KEY The SSH key to add to the root user --wait=SECONDS Block until CCI is finished provisioning for up to X seconds before returning. """ @@ -428,6 +430,12 @@ def execute(client, args): if args.get('--postinstall'): data['post_uri'] = args.get('--postinstall') + # Get the SSH key + if args.get('--key'): + key_id = resolve_id(SshKeyManager(client).resolve_ids, + args.get('--key'), 'SshKey') + data['ssh_key'] = key_id + t = Table(['Item', 'cost']) t.align['Item'] = 'r' t.align['cost'] = 'r' diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 52b7cc203..212152ba3 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -25,7 +25,7 @@ CLIRunnable, Table, KeyValueTable, FormattedItem, NestedDict, CLIAbort, blank, listing, SequentialOutput, gb, no_going_back, resolve_id, confirm, ArgumentError) -from SoftLayer import HardwareManager +from SoftLayer import HardwareManager, SshKeyManager class ListHardware(CLIRunnable): @@ -614,6 +614,7 @@ class CreateHardware(CLIRunnable): -n MBPS, --network=MBPS Network port speed in Mbps --controller=RAID The RAID configuration for the server. Defaults to None. + -k KEY, --key=KEY The SSH key to assign to the root user --dry-run, --test Do not create the server, just get a quote """ action = 'create' @@ -682,6 +683,12 @@ def execute(cls, client, args): else: raise CLIAbort('Invalid NIC speed specified.') + # Get the SSH key + if args.get('--key'): + key_id = resolve_id(SshKeyManager(client).resolve_ids, + args.get('--key'), 'SshKey') + order['ssh_key'] = key_id + # Begin output t = Table(['Item', 'cost']) t.align['Item'] = 'r' diff --git a/SoftLayer/CLI/modules/sshkey.py b/SoftLayer/CLI/modules/sshkey.py new file mode 100644 index 000000000..93af1585e --- /dev/null +++ b/SoftLayer/CLI/modules/sshkey.py @@ -0,0 +1,167 @@ +""" +usage: sl sshkey [] [...] [options] + +Manage SSH keys + +The available commands are: + add Add a new SSH key to your account + delete 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 +""" +# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. +# :license: BSD, see LICENSE for more details. + +from os.path import expanduser + +from SoftLayer import SshKeyManager +from SoftLayer.CLI import (CLIRunnable, Table, no_going_back) +from SoftLayer.CLI.helpers import (CLIAbort, resolve_id, KeyValueTable) + + +class AddSshKey(CLIRunnable): + """ +usage: sl sshkey add