From 1245b94975f64b1ae1695cd8a62484954a5d82a6 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Fri, 5 Apr 2013 17:15:53 -0500 Subject: [PATCH 0001/3227] Consistancy update --- SoftLayer/CLI/modules/dns.py | 72 ++++++++++++++++++++++-------------- SoftLayer/CLI/modules/ssl.py | 7 ++-- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index e4b101ecc..3f879190c 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -20,49 +20,49 @@ from SoftLayer.DNS import DNSManager -def add_zone_arguments(parser): - parser.add_argument('domain') - - -def add_record_arguments(parser): - add_zone_arguments(parser) - parser.add_argument('record') - - class DumpZone(CLIRunnable): """ -usage: sl dns print [options] +usage: sl dns print [options] print zone in BIND format + +Arguments: + Zone name (softlayer.com) """ action = "print" @staticmethod def execute(client, args): manager = DNSManager(client) - return manager.dump_zone(manager.get_zone(args[''])['id']) + return manager.dump_zone(manager.get_zone(args[''])['id']) class CreateZone(CLIRunnable): """ -usage: sl dns create [options] +usage: sl dns create [options] Create a zone + +Arguments: + Zone name (softlayer.com) """ action = 'create' @staticmethod def execute(client, args): manager = DNSManager(client) - manager.create_zone(args['']) - return "Created zone: %s" % args[''] + manager.create_zone(args['']) + return "Created zone: %s" % args[''] class DeleteZone(CLIRunnable): """ -usage: sl dns delete [options] +usage: sl dns delete [options] Delete zone + +Arguments: + Zone name (softlayer.com) """ action = 'delete' options = ['confirm'] @@ -70,9 +70,9 @@ class DeleteZone(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - if args['--really'] or no_going_back(args['']): - manager.delete_zone(args['']) - return "Deleted zone: %s" % args[''] + if args['--really'] or no_going_back(args['']): + manager.delete_zone(args['']) + return "Deleted zone: %s" % args[''] raise CLIAbort("Aborted.") @@ -110,12 +110,18 @@ def execute(client, args): class AddRecord(CLIRunnable): """ -usage: sl dns add [--ttl=TTL] [options] +usage: sl dns add [--ttl=TTL] [options] Add resource record +Arguments: + Zone name (softlayer.com) + Resource record (www) + Record type. [Options: A, AAAA, + CNAME, MX, NS, PTR, SPF, SRV, TXT] + Record data. NOTE: only minor validation is done + Options: - type Record type. [Options: A, AAAA, CNAME, MX, NS, PTR, SPF, SRV, TXT] --ttl=TTL Time to live [default: 7200] """ action = 'add' @@ -123,7 +129,7 @@ class AddRecord(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - zone = manager.get_zone(args[''])['id'] + zone = manager.get_zone(args[''])['id'] manager.create_record( zone, args[''], @@ -134,11 +140,15 @@ def execute(client, args): class EditRecord(CLIRunnable): """ -usage: sl dns edit [--data=DATA] [--ttl=TTL] [--id=ID] +usage: sl dns edit [--data=DATA] [--ttl=TTL] [--id=ID] [options] Update resource records (bulk/single) +Arguments: + Zone name (softlayer.com) + Resource record (www) + Options: --data=DATA --ttl=TTL Time to live [default: 7200] @@ -150,7 +160,7 @@ class EditRecord(CLIRunnable): def execute(client, args): manager = DNSManager(client) results = manager.search_record( - args[''], + args[''], args['']) for r in results: @@ -163,9 +173,13 @@ def execute(client, args): class RecordSearch(CLIRunnable): """ -usage: sl dns search [options] +usage: sl dns search [options] Look for a resource record by exact name + +Arguments: + Zone name (softlayer.com) + Resource record (www) """ action = 'search' @@ -173,7 +187,7 @@ class RecordSearch(CLIRunnable): def execute(client, args): manager = DNSManager(client) results = manager.search_record( - args[''], + args[''], args['']) t = Table(['id', 'type', 'ttl', 'data']) @@ -188,10 +202,14 @@ def execute(client, args): class RecordRemove(CLIRunnable): """ -usage: sl dns remove [--id=ID] [options] +usage: sl dns remove [--id=ID] [options] Remove resource records +Arguments: + Zone name (softlayer.com) + Resource record (www) + Options: --id=ID Remove only the given ID """ @@ -206,7 +224,7 @@ def execute(client, args): records = [{'id': args['--id']}] else: records = manager.search_record( - args[''], + args[''], args['']) if args['--really'] or no_going_back('yes'): diff --git a/SoftLayer/CLI/modules/ssl.py b/SoftLayer/CLI/modules/ssl.py index 0829d770c..6e3636d2b 100755 --- a/SoftLayer/CLI/modules/ssl.py +++ b/SoftLayer/CLI/modules/ssl.py @@ -15,6 +15,7 @@ # :license: BSD, see LICENSE for more details. from SoftLayer.CLI.helpers import CLIRunnable, no_going_back, Table, CLIAbort +from SoftLayer.CLI.helpers import blank from SoftLayer.SSL import SSLManager @@ -44,7 +45,7 @@ def execute(client, args): certificate['id'], certificate['commonName'], certificate['validityDays'], - certificate.get('notes', None) + certificate.get('notes', blank()) ]) t.sortby = args['--sortby'] return t @@ -54,7 +55,7 @@ class AddCertificate(CLIRunnable): """ usage: sl ssl add --crt=FILE --key=FILE [options] -Add SSL certificate +Add and upload SSL certificate details Options: --crt=FILE Certificate file @@ -96,7 +97,7 @@ def execute(client, args): class EditCertificate(CLIRunnable): """ -usage: sl ssl edit [options] +usage: sl ssl edit [options] Edit SSL certificate From 57e19bfda57ebfebdab6f16f2d93906bba4fd01d Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Tue, 9 Apr 2013 15:00:09 -0500 Subject: [PATCH 0002/3227] Added blockDevices to CCI.get_instance() --- SoftLayer/CCI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SoftLayer/CCI.py b/SoftLayer/CCI.py index 9e619e281..bccb10ac0 100644 --- a/SoftLayer/CCI.py +++ b/SoftLayer/CCI.py @@ -149,6 +149,7 @@ def get_instance(self, id, **kwargs): 'maxMemory', 'datacenter.name', 'activeTransaction.id', + 'blockDevices', 'blockDeviceTemplateGroup[id, name]', 'status.name', 'operatingSystem.softwareLicense.' From a9732c21ffac4398be213068a4c3caba415bc4ec Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 9 Apr 2013 15:29:52 -0500 Subject: [PATCH 0003/3227] Version Bump. Adds CHANGELOG --- CHANGELOG | 15 +++++++++++++++ SoftLayer/consts.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 000000000..7a6d5f02b --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,15 @@ +2.2.0 + + * Consistency changes/bug fixes + + * Added sphinx documentation + + * CCI: Adds Support for Additional Disks + + * 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(CCI): Adds the ability to filter list results for CCIs + + * API: Adds generator to transparently chunk SoftLayer requests into reasonable chunks diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index a7adc59d8..0b6a68321 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.1.0' +VERSION = 'v2.2.0' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3/' API_PRIVATE_ENDPOINT_REST = 'https://api.service.softlayer.com/rest/v3/' diff --git a/docs/conf.py b/docs/conf.py index d20eb215f..fca76bebf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,9 +49,9 @@ # built documents. # # The short X.Y version. -version = '2.1.0' +version = '2.2.0' # The full version, including alpha/beta/rc tags. -release = '2.1.0' +release = '2.2.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 05c08fcb8..45a5c916c 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( name='SoftLayer', - version='2.1.0', + version='2.2.0', description=description, long_description=long_description, author='SoftLayer Technologies, Inc.', From bbbfb4233d096a14fd92e922b80bdf93a53c0649 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 9 Apr 2013 18:15:25 -0500 Subject: [PATCH 0004/3227] Documentation Improvements --- CHANGELOG | 4 ++-- SoftLayer/API.py | 40 ++++++++++++++++++---------------- docs/_static/style.css | 7 ++++++ docs/api/client.rst | 2 +- docs/api/managers.rst | 2 +- docs/api/managers/cci.rst | 1 + docs/api/managers/dns.rst | 1 + docs/api/managers/firewall.rst | 1 + docs/api/managers/metadata.rst | 1 + docs/api/managers/ssl.rst | 1 + docs/conf.py | 1 + 11 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 docs/_static/style.css diff --git a/CHANGELOG b/CHANGELOG index 7a6d5f02b..4a4701907 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,7 @@ * Consistency changes/bug fixes - * Added sphinx documentation + * Added sphinx documentation. See it here: http://softlayer.github.com/softlayer-api-python-client * CCI: Adds Support for Additional Disks @@ -12,4 +12,4 @@ * CLI(CCI): Adds the ability to filter list results for CCIs - * API: Adds generator to transparently chunk SoftLayer requests into reasonable chunks + * 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/API.py b/SoftLayer/API.py index 20639e4cc..bfe27ffd4 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -200,16 +200,11 @@ def call(self, service, method, *args, **kwargs): :param service: the name of the SoftLayer API service :param method: the method to call on the service - :param \*args: (optional) arguments for the remote call - :param id: (optional) id for the resource - :param mask: (optional) object mask - :param dict filter: (optional) filter dict - :param dict headers: (optional) optional XML-RPC headers - :param dict raw_headers: (optional) HTTP transport headers - :param int limit: (optional) return at most this many results - :param int offset: (optional) offset results by this many - :param boolean iter: (optional) if True, returns a generator with the - results + :param \*args: same optional arguments that ``Service.call`` takes + :param \*\*kwargs: same optional keyword arguments that + ``Service.call`` takes + + :param service: the name of the SoftLayer API service Usage: >>> client['Account'].getVirtualGuests(mask="id", limit=10) @@ -276,9 +271,9 @@ def iter_call(self, service, method, :param service: the name of the SoftLayer API service :param method: the method to call on the service :param integer chunk: result size for each API call - :param \*args: same optional arguments that ``Client.call`` takes - :param \*\*kwargs: same optional keyword arguments that ``Client.call`` - takes + :param \*args: same optional arguments that ``Service.call`` takes + :param \*\*kwargs: same optional keyword arguments that + ``Service.call`` takes """ if chunk <= 0: @@ -386,9 +381,16 @@ def call(self, name, *args, **kwargs): """ Make a SoftLayer API call :param method: the method to call on the service - :param \*args: same optional arguments that ``Client.call`` takes - :param \*\*kwargs: same optional keyword arguments that ``Client.call`` - takes + :param \*args: (optional) arguments for the remote call + :param id: (optional) id for the resource + :param mask: (optional) object mask + :param dict filter: (optional) filter dict + :param dict headers: (optional) optional XML-RPC headers + :param dict raw_headers: (optional) HTTP transport headers + :param int limit: (optional) return at most this many results + :param int offset: (optional) offset results by this many + :param boolean iter: (optional) if True, returns a generator with the + results Usage: >>> client['Account'].getVirtualGuests(mask="id", limit=10) @@ -404,9 +406,9 @@ def iter_call(self, name, *args, **kwargs): :param method: the method to call on the service :param integer chunk: result size for each API call - :param \*args: same optional arguments that ``Client.call`` takes - :param \*\*kwargs: same optional keyword arguments that ``Client.call`` - takes + :param \*args: same optional arguments that ``Service.call`` takes + :param \*\*kwargs: same optional keyword arguments that + ``Service.call`` takes Usage: >>> gen = client['Account'].getVirtualGuests(iter=True) diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 000000000..5d04fa571 --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,7 @@ +@import url("nature.css"); + +p.deprecated { + background-color: #ffe4e4; + border: 1px solid #f66; + padding: 0.2em; +} \ No newline at end of file diff --git a/docs/api/client.rst b/docs/api/client.rst index 0b627988d..af405c41b 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -53,7 +53,7 @@ Retreive a ticket using Object Masks. id=123456, mask="mask[updates, assignedUser, attachedHardware.datacenter]") -Now add an update to the ticket. +Now add an update to the ticket with `Ticket.addUpdate `_. This uses a parameter, which translate to positional arguments in the order that they appear in the API docs. :: update = client['Ticket'].addUpdate({'entry' : 'Hello!'}, id=123456) diff --git a/docs/api/managers.rst b/docs/api/managers.rst index 34e7cad99..1f866357a 100644 --- a/docs/api/managers.rst +++ b/docs/api/managers.rst @@ -9,7 +9,7 @@ Managers >>> 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. +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 diff --git a/docs/api/managers/cci.rst b/docs/api/managers/cci.rst index 17c6c7055..5af2a9a1d 100644 --- a/docs/api/managers/cci.rst +++ b/docs/api/managers/cci.rst @@ -2,4 +2,5 @@ .. automodule:: SoftLayer.CCI :members: + :inherited-members: :undoc-members: diff --git a/docs/api/managers/dns.rst b/docs/api/managers/dns.rst index aae1544ac..047a1ea49 100644 --- a/docs/api/managers/dns.rst +++ b/docs/api/managers/dns.rst @@ -2,4 +2,5 @@ .. automodule:: SoftLayer.DNS :members: + :inherited-members: :undoc-members: diff --git a/docs/api/managers/firewall.rst b/docs/api/managers/firewall.rst index 87ecc8828..ec4eda1a2 100644 --- a/docs/api/managers/firewall.rst +++ b/docs/api/managers/firewall.rst @@ -2,4 +2,5 @@ .. automodule:: SoftLayer.firewall :members: + :inherited-members: :undoc-members: diff --git a/docs/api/managers/metadata.rst b/docs/api/managers/metadata.rst index ea41d181b..e5cb87b2d 100644 --- a/docs/api/managers/metadata.rst +++ b/docs/api/managers/metadata.rst @@ -2,6 +2,7 @@ .. automodule:: SoftLayer.metadata :members: + :inherited-members: :undoc-members: .. autoattribute:: SoftLayer.metadata.METADATA_ATTRIBUTES \ No newline at end of file diff --git a/docs/api/managers/ssl.rst b/docs/api/managers/ssl.rst index 3676f18fd..17c9e053e 100644 --- a/docs/api/managers/ssl.rst +++ b/docs/api/managers/ssl.rst @@ -2,4 +2,5 @@ .. automodule:: SoftLayer.SSL :members: + :inherited-members: :undoc-members: diff --git a/docs/conf.py b/docs/conf.py index fca76bebf..e8ddfdeb8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -122,6 +122,7 @@ # 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 ef3ff6429a07a95dd1721e61c7d13ca8a6c09af1 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Wed, 10 Apr 2013 16:46:06 -0500 Subject: [PATCH 0005/3227] Removed all domain/zone naming ambiguity from DNS manager --- SoftLayer/DNS.py | 46 ++++++++++++++++---------------- SoftLayer/tests/API/dns_tests.py | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/SoftLayer/DNS.py b/SoftLayer/DNS.py index cc99be44f..54a04c69d 100644 --- a/SoftLayer/DNS.py +++ b/SoftLayer/DNS.py @@ -27,7 +27,7 @@ def __init__(self, client): """ self.client = client - self.domain = self.client['Dns_Domain'] + self.service = self.client['Dns_Domain'] self.record = self.client['Dns_Domain_ResourceRecord'] def list_zones(self, **kwargs): @@ -38,32 +38,32 @@ def list_zones(self, **kwargs): """ return self.client['Account'].getDomains(**kwargs) - def get_zone(self, domain): - """ Get a domain/zone and its records. + def get_zone(self, zone): + """ Get a zone and its records. - :param domain: the domain/zone name + :param zone: the zone name """ - domain = domain.lower() - results = self.domain.getByDomainName( - domain, + zone = zone.lower() + results = self.service.getByDomainName( + zone, mask={'resourceRecords': {}}) - matches = filter(lambda x: x['name'].lower() == domain, results) + matches = filter(lambda x: x['name'].lower() == zone, results) try: return matches[0] except IndexError: - raise DNSZoneNotFound(domain) + raise DNSZoneNotFound(zone) - def create_zone(self, domain, serial=None): - """ Create a zone for the specified domain. + def create_zone(self, zone, serial=None): + """ Create a zone for the specified zone. - :param domain: the domain/zone name to create + :param zone: the zone name to create :param serial: serial value on the zone (default: strftime(%Y%m%d01)) """ - return self.domain.createObject({ - 'name': domain, + return self.service.createObject({ + 'name': zone, 'serial': serial or strftime('%Y%m%d01')}) def delete_zone(self, id): @@ -72,7 +72,7 @@ def delete_zone(self, id): :param integer id: the zone ID to delete """ - return self.domain.deleteObject(id=id) + return self.service.deleteObject(id=id) def edit_zone(self, zone): """ Update an existing zone with the options provided. The provided @@ -82,12 +82,12 @@ def edit_zone(self, zone): :param dict zone: the zone to update """ - self.domain.editObject(zone) + self.service.editObject(zone) def create_record(self, id, record, type, data, ttl=60): """ Create a resource record on a domain. - :param integer id: the domain's ID + :param integer id: the zone's ID :param record: the name of the record to add :param type: the type of record (A, AAAA, CNAME, MX, SRV, TXT, etc.) :param data: the record's value @@ -109,16 +109,16 @@ def delete_record(self, recordid): """ self.record.deleteObject(id=recordid) - def search_record(self, domain, record): - """ Search for records on a domain that match a specific name. + def search_record(self, zone, record): + """ Search for records on a zone that match a specific name. Useful for validating whether a record exists or that it has the correct value. - :param domain: the domain/zone name in which to search. + :param zone: the zone name in which to search. :param record: the record name to search for """ - rrs = self.get_zone(domain)['resourceRecords'] + rrs = self.get_zone(zone)['resourceRecords'] records = filter(lambda x: x['host'].lower() == record.lower(), rrs) return records @@ -135,7 +135,7 @@ def edit_record(self, record): def dump_zone(self, id): """ Retrieve a zone dump in BIND format. - :param integer id: The zone/domain ID to dump + :param integer id: The zone ID to dump """ - return self.domain.getZoneFileContents(id=id) + return self.service.getZoneFileContents(id=id) diff --git a/SoftLayer/tests/API/dns_tests.py b/SoftLayer/tests/API/dns_tests.py index 5916f5592..a6cf667ab 100644 --- a/SoftLayer/tests/API/dns_tests.py +++ b/SoftLayer/tests/API/dns_tests.py @@ -21,7 +21,7 @@ def setUp(self): self.dns_client = SoftLayer.DNSManager(self.client) def test_init_exercise(self): - self.assertTrue(hasattr(self.dns_client, 'domain')) + self.assertTrue(hasattr(self.dns_client, 'service')) self.assertTrue(hasattr(self.dns_client, 'record')) def test_list_zones(self): From 1f846cb48100012d576dc617b57b91b0ea2a23b2 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 11 Apr 2013 10:12:56 -0500 Subject: [PATCH 0006/3227] Minor bug fix (mostly for devs) * --format now defaults to table if isn't set to anything from docopt. This happens if [options] is not included in the usage for a command. --- SoftLayer/CLI/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index f77b89c1c..114544cf1 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -189,7 +189,7 @@ def main(args=sys.argv[1:], env=Environment()): # Do the thing data = action.execute(client, submodule_args) if data: - format = submodule_args.get('--format') + format = submodule_args.get('--format', 'table') if format not in ['raw', 'table']: raise ArgumentError('Invalid Format "%s"' % format) s = format_output(data, fmt=format) From 5c6f321b48b63dd297152fbb254002afd9537f97 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Thu, 11 Apr 2013 10:45:44 -0500 Subject: [PATCH 0007/3227] CLI: fix bug in helpers.no_going_back() helpers.valid_response() expects a list of str for matching true values. no_going_back() now casts the confirmation value to str() before passing it to valid_response() --- SoftLayer/CLI/helpers.py | 2 +- SoftLayer/tests/CLI/helper_tests.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index 6e535dd35..df803a0f3 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -90,7 +90,7 @@ def no_going_back(confirmation): return valid_response( 'This action cannot be undone! ' 'Type "%s" or press Enter to abort: ' % confirmation, - confirmation) + str(confirmation)) class CLIHalt(SystemExit): diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 889522200..b35fc7287 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -59,6 +59,12 @@ def test_do_or_die(self, raw_input_mock): result = cli.no_going_back(confirmed) self.assertTrue(result) + # no_going_back should cast int's to str() + confirmed = '4712309182309' + raw_input_mock.return_value = confirmed + result = cli.no_going_back(int(confirmed)) + self.assertTrue(result) + confirmed = None raw_input_mock.return_value = '' result = cli.no_going_back(confirmed) From 11808a561a15ded056b645352f9d2e7e3fc40c7f Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Thu, 11 Apr 2013 11:48:41 -0500 Subject: [PATCH 0008/3227] DNS fixups, CLI dns cleanups --- SoftLayer/CLI/modules/dns.py | 42 +++++++++++++++++++++----------- SoftLayer/DNS.py | 3 ++- SoftLayer/tests/API/dns_tests.py | 6 ++++- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 3f879190c..9f208dff1 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -17,7 +17,7 @@ # :license: BSD, see LICENSE for more details. from SoftLayer.CLI import CLIRunnable, no_going_back, Table, CLIAbort -from SoftLayer.DNS import DNSManager +from SoftLayer.DNS import DNSManager, DNSZoneNotFound class DumpZone(CLIRunnable): @@ -34,7 +34,10 @@ class DumpZone(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - return manager.dump_zone(manager.get_zone(args[''])['id']) + try: + return manager.dump_zone(manager.get_zone(args[''])['id']) + except DNSZoneNotFound: + raise CLIAbort("No zone found matching: %s" % args['']) class CreateZone(CLIRunnable): @@ -52,7 +55,6 @@ class CreateZone(CLIRunnable): def execute(client, args): manager = DNSManager(client) manager.create_zone(args['']) - return "Created zone: %s" % args[''] class DeleteZone(CLIRunnable): @@ -72,7 +74,6 @@ def execute(client, args): manager = DNSManager(client) if args['--really'] or no_going_back(args['']): manager.delete_zone(args['']) - return "Deleted zone: %s" % args[''] raise CLIAbort("Aborted.") @@ -129,7 +130,10 @@ class AddRecord(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - zone = manager.get_zone(args[''])['id'] + try: + zone = manager.get_zone(args[''])['id'] + except DNSZoneNotFound: + raise CLIAbort("No zone found matching: %s" % args['']) manager.create_record( zone, args[''], @@ -159,9 +163,12 @@ class EditRecord(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - results = manager.search_record( - args[''], - args['']) + try: + results = manager.search_record( + args[''], + args['']) + except DNSZoneNotFound: + raise CLIAbort("No zone found matching: %s" % args['']) for r in results: if args['--id'] and r['id'] != args['--id']: @@ -186,9 +193,13 @@ class RecordSearch(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - results = manager.search_record( - args[''], - args['']) + results = [] + try: + results = manager.search_record( + args[''], + args['']) + except DNSZoneNotFound: + raise CLIAbort("No zone found matching: %s" % args['']) t = Table(['id', 'type', 'ttl', 'data']) @@ -223,9 +234,12 @@ def execute(client, args): if args['--id']: records = [{'id': args['--id']}] else: - records = manager.search_record( - args[''], - args['']) + try: + records = manager.search_record( + args[''], + args['']) + except DNSZoneNotFound: + raise CLIAbort("No zone found matching: %s" % args['']) if args['--really'] or no_going_back('yes'): t = Table(['record']) diff --git a/SoftLayer/DNS.py b/SoftLayer/DNS.py index 54a04c69d..8bb1fcb6e 100644 --- a/SoftLayer/DNS.py +++ b/SoftLayer/DNS.py @@ -64,7 +64,8 @@ def create_zone(self, zone, serial=None): """ return self.service.createObject({ 'name': zone, - 'serial': serial or strftime('%Y%m%d01')}) + 'serial': serial or strftime('%Y%m%d01'), + "resourceRecords": {}}) def delete_zone(self, id): """ Delete a zone by its ID. diff --git a/SoftLayer/tests/API/dns_tests.py b/SoftLayer/tests/API/dns_tests.py index a6cf667ab..2de214d18 100644 --- a/SoftLayer/tests/API/dns_tests.py +++ b/SoftLayer/tests/API/dns_tests.py @@ -11,7 +11,7 @@ import unittest2 as unittest except ImportError: import unittest # NOQA -from mock import MagicMock +from mock import MagicMock, ANY class DNSTests(unittest.TestCase): @@ -54,6 +54,10 @@ def test_create_zone(self): {'name': 'example.com'} res = self.dns_client.create_zone('example.com') + + self.client.__getitem__().createObject.assert_called_once_with( + {'name': 'example.com', "resourceRecords": {}, "serial": ANY}) + self.assertEqual(res, {'name': 'example.com'}) def test_delete_zone(self): From 6634a79fd1455bb9157c002fab6c185d63ecb5cf Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Thu, 11 Apr 2013 12:08:02 -0500 Subject: [PATCH 0009/3227] Fixed major bug in SSL --- SoftLayer/CLI/modules/firewall.py | 11 +++++++++-- SoftLayer/CLI/modules/ssl.py | 17 ++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py index 44c0edf0f..7175d814b 100755 --- a/SoftLayer/CLI/modules/firewall.py +++ b/SoftLayer/CLI/modules/firewall.py @@ -10,6 +10,7 @@ # :license: BSD, see LICENSE for more details. from SoftLayer.CLI import CLIRunnable, Table, listing +from SoftLayer.CLI.helpers import blank from SoftLayer.firewall import FirewallManager @@ -32,14 +33,20 @@ def execute(client, args): features = [] if vlan['highAvailabilityFirewallFlag']: features.append('HA') + + if features: + listing(features, separator=',') + else: + feature_list = blank() + t.add_row([ vlan['vlanNumber'], 'dedicated', - listing(features, separator=','), + feature_list, ]) shared_vlan = filter(lambda x: not x['dedicatedFirewallFlag'], fwvlans) for vlan in shared_vlan: - t.add_row([vlan['vlanNumber'], 'standard', '']) + t.add_row([vlan['vlanNumber'], 'standard', blank()]) return t diff --git a/SoftLayer/CLI/modules/ssl.py b/SoftLayer/CLI/modules/ssl.py index 6e3636d2b..393655281 100755 --- a/SoftLayer/CLI/modules/ssl.py +++ b/SoftLayer/CLI/modules/ssl.py @@ -77,22 +77,18 @@ def execute(client, args): template['certificate'] = open(args['--crt']).read() template['privateKey'] = open(args['--key']).read() if args['--csr']: - template['intermediateCertificate'] = \ + template['certificateSigningRequest'] = \ open(args['--csr']).read() if args['--icc']: - template['certificateSigningRequest'] = \ + template['intermediateCertificate'] = \ open(args['--icc']).read() except IOError: - raise ValueError("File does not exist") - exit(1) + raise CLIAbort("File does not exist") manager = SSLManager(client) - cert = manager.add_certificate(template) - return ( - "Created certificate: %s" % cert['commonName'], - "Created certificate: %s" % cert['name']) + manager.add_certificate(template) class EditCertificate(CLIRunnable): @@ -118,9 +114,9 @@ def execute(client, args): if args['--key']: template['privateKey'] = open(args['--key']).read() if args['--csr']: - template['intermediateCertificate'] = open(args['--csr']).read() + template['certificateSigningRequest'] = open(args['--csr']).read() if args['--icc']: - template['certificateSigningRequest'] = open(args['--icc']).read() + template['intermediateCertificate'] = open(args['--icc']).read() if args['--notes']: template['notes'] = args['--notes'] @@ -142,7 +138,6 @@ def execute(client, args): manager = SSLManager(client) if args['--really'] or no_going_back('yes'): manager.remove_certificate(args['']) - return "Deleted certificate: %s" % args[''] raise CLIAbort("Aborted.") From 72207afe5a852429a3e8de9259392d3a9e9e49f1 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Thu, 11 Apr 2013 12:14:20 -0500 Subject: [PATCH 0010/3227] fixed regression in firewall --- SoftLayer/CLI/modules/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py index 7175d814b..37982b0f6 100755 --- a/SoftLayer/CLI/modules/firewall.py +++ b/SoftLayer/CLI/modules/firewall.py @@ -35,7 +35,7 @@ def execute(client, args): features.append('HA') if features: - listing(features, separator=',') + feature_list = listing(features, separator=',') else: feature_list = blank() From b0531816877c527c7f63197fe1e423f68635f98e Mon Sep 17 00:00:00 2001 From: SLphil Date: Fri, 12 Apr 2013 10:29:26 -0500 Subject: [PATCH 0011/3227] Adding in Adding in manager and CLI for hardware device listing and details. Still needs tests... --- SoftLayer/CLI/modules/hardware.py | 105 ++++++++++++++++++++++++++++++ SoftLayer/hardware.py | 53 +++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 SoftLayer/CLI/modules/hardware.py create mode 100644 SoftLayer/hardware.py diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py new file mode 100644 index 000000000..bd1c581cd --- /dev/null +++ b/SoftLayer/CLI/modules/hardware.py @@ -0,0 +1,105 @@ +""" +usage: sl hardware [] [...] [options] + sl hardware [-h | --help] + +Manage hardware + +The available commands are: + list List hardware devices + detail Retrieve hardware details +""" +from SoftLayer.CLI.helpers import CLIRunnable, Table, FormattedItem, NestedDict, blank, listing +from SoftLayer.hardware import HardwareManager + + +class ListHardware(CLIRunnable): + """ +usage: sl hardware list [options] + +List hardware servers on the acount +""" + action = 'list' + + @staticmethod + def execute(client, args): + manager = HardwareManager(client) + + servers = manager.list_hardware() + t = Table(['id', 'hostname', 'domain']) + for server in servers: + t.add_row([ + server['id'], + server['hostname'], + server['domain'] + ]) + + return t + + +class HardwareDetails(CLIRunnable): + """ +usage: sl hardware detail [--passwords] [--price] [options] + +Get details for a hardware device + +Options: + --passwords Show passwords (check over your shoulder!) + --price Show associated prices +""" + action = 'detail' + + @staticmethod + def execute(client, args): + hardware = HardwareManager(client) + + t = Table(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + + hardware_id = hardware, args.get('') + result = hardware.get_hardware(hardware_id[1]) + result = NestedDict(result) + + t.add_row(['id', result['id']]) + t.add_row(['hostname', result['fullyQualifiedDomainName']]) + t.add_row(['datacenter', result['datacenter'].get('name', blank())]) + t.add_row(['provisionDate', result['provisionDate']]) + t.add_row(['public_ip', result.get('primaryIpAddress', blank())]) + t.add_row( + ['private_ip', result.get('primaryBackendIpAddress', blank())]) + t.add_row([ + 'os', + FormattedItem( + result['operatingSystem']['softwareLicense'] + ['softwareDescription'].get('referenceCode', blank()), + result['operatingSystem']['softwareLicense'] + ['softwareDescription'].get('name', blank()) + )]) + if result.get('notes'): + t.add_row(['notes', result['notes']]) + + if args.get('--price'): + t.add_row(['price rate', result['billingItem']['recurringFee']]) + + if args.get('--passwords'): + user_strs = [] + for item in result['operatingSystem']['passwords']: + user_strs.append( + "%s %s" % (item['username'], item['password'])) + t.add_row(['users', listing(user_strs)]) + + tag_row = [] + for tag in result['tagReferences']: + tag_row.append(tag['tag']['name']) + + if tag_row: + t.add_row(['tags', listing(tag_row, separator=',')]) + + ptr_domains = client['Hardware_Server'].\ + getReverseDomainRecords(id=hardware_id[1]) + + for ptr_domain in ptr_domains: + for ptr in ptr_domain['resourceRecords']: + t.add_row(['ptr', ptr['data']]) + + return t diff --git a/SoftLayer/hardware.py b/SoftLayer/hardware.py new file mode 100644 index 000000000..31ef8b31f --- /dev/null +++ b/SoftLayer/hardware.py @@ -0,0 +1,53 @@ +__all__ = ["HardwareManager"] + + +class HardwareManager(object): + """ Manages hardware devices. """ + + def __init__(self, client): + """ HardwareManager initialization. + + :param SoftLayer.API.Client client: an API client instance + + """ + self.client = client + self.hardware = self.client['Hardware_Server'] + self.account = self.client['Account'] + + def list_hardware(self): + """ List all hardware. + + """ + + return self.account.getHardware() + + def get_hardware(self, id, **kwargs): + """ Get details about a hardware device + + :param integer id: the hardware ID + + """ + + if 'mask' not in kwargs: + items = set([ + 'id', + 'globalIdentifier', + 'fullyQualifiedDomainName', + 'hostname', + 'domain', + 'provisionDate', + 'notes', + 'privateNetworkOnlyFlag', + 'primaryBackendIpAddress', + 'primaryIpAddress', + 'datacenter.name', + 'activeTransaction.id', + 'operatingSystem.softwareLicense.' + 'softwareDescription[manufacturer,name,version,referenceCode]', + 'operatingSystem.passwords[username,password]', + 'billingItem.recurringFee', + 'tagReferences[id,tag[name,id]]', + ]) + kwargs['mask'] = "mask[%s]" % ','.join(items) + + return self.hardware.getObject(id=id, **kwargs) From 97d03fc90eb4ce88a57c0122047002c0c4991452 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 11 Apr 2013 10:34:59 -0500 Subject: [PATCH 0012/3227] Small Usability Fix If [options] is not in the submodule's doc block don't add the additional options. --- SoftLayer/CLI/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 114544cf1..d0c94762b 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -117,7 +117,8 @@ def parse_submodule_args(submodule, args): -y, --really Confirm all prompt actions """ - arg_doc += """ + if '[options]' in arg_doc: + arg_doc += """ Standard Options: --format=ARG Output format. [Options: table, raw] [Default: %s] -C FILE --config=FILE Config file location. [Default: ~/.softlayer] From bc5621e4e7942988266bb6e1d2b3241c6c7b12ed Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 23 Apr 2013 13:55:43 -0500 Subject: [PATCH 0013/3227] Adds Generic Auth Concept to Client API: Adds 'auth' keyword argument to SoftLayer.Client(). This object should respond to get_headers(). If just username and api_key are passed in, BasicAuthentication will be used for auth. --- SoftLayer/API.py | 42 ++++++++++++++++++++--------- SoftLayer/__init__.py | 7 ++--- SoftLayer/tests/API/client_tests.py | 22 +++++++++++++++ 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index bfe27ffd4..ea1141229 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -17,6 +17,23 @@ API_BASE_URL = API_PUBLIC_ENDPOINT +class BasicAuthentication(object): + 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. @@ -33,6 +50,8 @@ class Client(object): network. :param integer timeout: timeout for API requests :param boolean verbose: prints details about every HTTP request if true + :param auth: an object which responds to get_headers() to be inserted into + the xml-rpc headers. Example: `BasicAuthentication` Usage: @@ -46,16 +65,19 @@ class Client(object): _prefix = "SoftLayer_" def __init__(self, service_name=None, id=None, username=None, api_key=None, - endpoint_url=None, timeout=None, verbose=False): + endpoint_url=None, timeout=None, verbose=False, auth=None): self._service_name = service_name self.verbose = verbose self._headers = {} self._raw_headers = {} - self.username = username or API_USERNAME or \ - os.environ.get('SL_USERNAME') or '' - self.api_key = api_key or API_KEY or os.environ.get('SL_API_KEY') or '' - self.set_authentication(self.username, self.api_key) + self.auth = auth + if self.auth is None: + username = username or API_USERNAME or \ + os.environ.get('SL_USERNAME') or '' + api_key = api_key or API_KEY or os.environ.get('SL_API_KEY') or '' + self.auth = BasicAuthentication(username, api_key) + self.set_authentication(username, api_key) if id is not None: self.set_init_parameter(int(id)) @@ -226,11 +248,7 @@ def call(self, service, method, *args, **kwargs): offset = kwargs.get('offset', 0) if headers is None: - headers = { - 'authenticate': { - 'username': self.username, - 'apiKey': self.api_key, - }} + headers = self.auth.get_headers() http_headers = { 'User-Agent': USER_AGENT, @@ -366,8 +384,8 @@ def call_handler(*args, **kwargs): return call_handler def __repr__(self): - return "" \ - % (self._endpoint_url, self.username) + return "" \ + % (self._endpoint_url, self.auth) __str__ = __repr__ diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 920e5a73b..5a5eef472 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -16,7 +16,8 @@ """ from SoftLayer.consts import VERSION -from API import Client, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT +from API import ( + Client, BasicAuthentication, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) from DNS import DNSManager from CCI import CCIManager from metadata import MetadataManager @@ -27,6 +28,6 @@ __author__ = 'SoftLayer Technologies, Inc.' __license__ = 'The BSD License' __copyright__ = 'Copyright 2013 SoftLayer Technologies, Inc.' -__all__ = ['Client', 'SoftLayerError', 'SoftLayerAPIError', - 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', +__all__ = ['Client', 'BasicAuthentication', 'SoftLayerError', + 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', 'DNSManager', 'CCIManager', 'MetadataManager'] diff --git a/SoftLayer/tests/API/client_tests.py b/SoftLayer/tests/API/client_tests.py index 072167957..79227c61f 100644 --- a/SoftLayer/tests/API/client_tests.py +++ b/SoftLayer/tests/API/client_tests.py @@ -339,3 +339,25 @@ def test_iter_call(self, _call): AttributeError, lambda: list(self.client.iter_call( 'SERVICE', 'METHOD', iter=True, chunk=0))) + + +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) From 44e39aa3393b4726df70be334b4255499aa37a23 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 23 Apr 2013 14:04:51 -0500 Subject: [PATCH 0014/3227] CLI: Alphabetizes commands in `sl metadata` --- SoftLayer/CLI/modules/metadata.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index 6289e9212..9cc1a797b 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -6,19 +6,19 @@ resources. The available commands are: - datacenter Datacenter name - backend_mac Backend mac addresses - ip Primary ip address backend_ip Primary backend ip address - tags Tags - hostname Hostname - fqdn Fully qualified domain name - user_data User-defined data + backend_mac Backend mac addresses + datacenter Datacenter name datacenter_id Datacenter id + fqdn Fully qualified domain name frontend_mac Frontend mac addresses - provision_state Provision state + hostname Hostname id Id + ip Primary ip address network Details about either the public or private network + provision_state Provision state + tags Tags + user_data User-defined data """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. From cc6e1b8367217c7437b33767ea7990e37e453bea Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 23 Apr 2013 15:05:01 -0500 Subject: [PATCH 0015/3227] API: Adds AuthenticationBase --- SoftLayer/API.py | 7 ++++++- SoftLayer/tests/API/client_tests.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index ea1141229..b6b6d2b64 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -17,7 +17,12 @@ API_BASE_URL = API_PUBLIC_ENDPOINT -class BasicAuthentication(object): +class AuthenticationBase(object): + def get_headers(self): + raise NotImplementedError + + +class BasicAuthentication(AuthenticationBase): def __init__(self, username, api_key): self.username = username self.api_key = api_key diff --git a/SoftLayer/tests/API/client_tests.py b/SoftLayer/tests/API/client_tests.py index 79227c61f..dc4cfbe22 100644 --- a/SoftLayer/tests/API/client_tests.py +++ b/SoftLayer/tests/API/client_tests.py @@ -341,6 +341,12 @@ def test_iter_call(self, _call): 'SERVICE', 'METHOD', iter=True, chunk=0))) +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') From 0e57d30405ad20501a75700d5ec0ecbcedf94648 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 23 Apr 2013 15:41:51 -0500 Subject: [PATCH 0016/3227] CLI: Config file now created with mode 0600 when using `sl config setup` Resolves #91 --- SoftLayer/CLI/modules/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/config.py b/SoftLayer/CLI/modules/config.py index 3604c99e1..12870f184 100644 --- a/SoftLayer/CLI/modules/config.py +++ b/SoftLayer/CLI/modules/config.py @@ -56,7 +56,8 @@ def execute(cls, client, args): if endpoint_url: config.set('softlayer', 'endpoint_url', endpoint_url) - f = open(config_path, 'w') + f = os.fdopen( + os.open(config_path, os.O_WRONLY | os.O_CREAT, 0600), 'w') try: config.write(f) finally: From 3b47667c4e7329ce0559d11fb211f2c5d22c7244 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 6 May 2013 11:04:44 -0500 Subject: [PATCH 0017/3227] Hardware Improvements * Adds ip addresses, datacenter, CPU/memory details to hardware list * Adds 'identity' logic to hardware so you can get details based on an id, hostname or ip address * Adds Hardware Manager tests * Cleans up filters for listing CCIs a bit * Adds hardware to command list so people can know that it exists. (also alphabatizes that list) --- SoftLayer/CCI.py | 15 +++--- SoftLayer/CLI/core.py | 11 ++-- SoftLayer/CLI/helpers.py | 6 ++- SoftLayer/CLI/modules/hardware.py | 50 ++++++++++++++++--- SoftLayer/__init__.py | 3 +- SoftLayer/hardware.py | 61 +++++++++++++++++++++-- SoftLayer/tests/API/hardware_tests.py | 72 +++++++++++++++++++++++++++ SoftLayer/tests/CLI/helper_tests.py | 15 +++++- 8 files changed, 204 insertions(+), 29 deletions(-) create mode 100644 SoftLayer/tests/API/hardware_tests.py diff --git a/SoftLayer/CCI.py b/SoftLayer/CCI.py index bccb10ac0..068adf2eb 100644 --- a/SoftLayer/CCI.py +++ b/SoftLayer/CCI.py @@ -7,11 +7,11 @@ :license: BSD, see LICENSE for more details. """ import socket +from time import sleep +from itertools import repeat from SoftLayer.exceptions import SoftLayerError from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin -from time import sleep -from itertools import repeat class CCICreateMissingRequired(SoftLayerError): @@ -67,7 +67,6 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, 'datacenter.name', 'activeTransaction.transactionStatus[friendlyName,name]', 'status.name', - 'tagReferences[id,tag[name,id]]', ]) kwargs['mask'] = "mask[%s]" % ','.join(items) @@ -182,11 +181,11 @@ def reload_instance(self, id): return self.guest.reloadCurrentOperatingSystemConfiguration(id=id) 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): + 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): required = [cpus, memory, hostname, domain] diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index d0c94762b..2626ecc9f 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -6,15 +6,16 @@ SoftLayer Command-line Client The available commands are: - firewall Firewall rule and security management - image Manages compute and flex images - ssl Manages SSL 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 + 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. diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index df803a0f3..47be55119 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -11,7 +11,7 @@ from prettytable import PrettyTable __all__ = ['Table', 'CLIRunnable', 'FormattedItem', 'valid_response', - 'confirm', 'no_going_back', 'mb_to_gb', 'listing', 'CLIAbort', + 'confirm', 'no_going_back', 'mb_to_gb', 'gb', 'listing', 'CLIAbort', 'NestedDict'] @@ -33,6 +33,10 @@ def mb_to_gb(megabytes): return FormattedItem(megabytes, "%dG" % (float(megabytes) / 1024)) +def gb(gigabytes): + return FormattedItem(gigabytes, "%dG" % float(gigabytes)) + + def blank(): """ Returns FormatedItem to make pretty output use a dash and raw formatting to use NULL""" diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index bd1c581cd..18858f037 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -7,11 +7,29 @@ The available commands are: list List hardware devices detail Retrieve hardware details + +For several commands, will be asked for. This can be the id, +hostname or the ip address for a piece of hardware. """ -from SoftLayer.CLI.helpers import CLIRunnable, Table, FormattedItem, NestedDict, blank, listing +from SoftLayer.CLI.helpers import ( + CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, gb) from SoftLayer.hardware import HardwareManager +def resolve_id(manager, identifier): + ids = manager.resolve_ids(identifier) + + if len(ids) == 0: + raise CLIAbort("Error: Unable to find hardware '%s'" % identifier) + + if len(ids) > 1: + raise CLIAbort( + "Error: Multiple hardware found for '%s': %s" % + (identifier, ', '.join([str(_id) for _id in ids]))) + + return ids[0] + + class ListHardware(CLIRunnable): """ usage: sl hardware list [options] @@ -25,12 +43,24 @@ def execute(client, args): manager = HardwareManager(client) servers = manager.list_hardware() - t = Table(['id', 'hostname', 'domain']) + t = Table([ + 'id', + 'datacenter', + 'host', + 'cores', + 'memory', + 'primary_ip', + 'backend_ip' + ]) for server in servers: t.add_row([ server['id'], - server['hostname'], - server['domain'] + server.get('datacenter', {}).get('name', 'unknown'), + server['fullyQualifiedDomainName'], + server['processorCoreAmount'], + gb(server['memoryCapacity']), + server.get('primaryIpAddress', blank()), + server.get('primaryBackendIpAddress', blank()), ]) return t @@ -56,17 +86,21 @@ def execute(client, args): t.align['Name'] = 'r' t.align['Value'] = 'l' - hardware_id = hardware, args.get('') - result = hardware.get_hardware(hardware_id[1]) + hardware_id = resolve_id(hardware, args.get('')) + result = hardware.get_hardware(hardware_id) result = NestedDict(result) t.add_row(['id', result['id']]) t.add_row(['hostname', result['fullyQualifiedDomainName']]) + t.add_row(['status', result['hardwareStatus']['status']]) t.add_row(['datacenter', result['datacenter'].get('name', blank())]) + t.add_row(['cores', result['processorCoreAmount']]) + t.add_row(['memory', result['memoryCapacity']]) t.add_row(['provisionDate', result['provisionDate']]) t.add_row(['public_ip', result.get('primaryIpAddress', blank())]) t.add_row( ['private_ip', result.get('primaryBackendIpAddress', blank())]) + t.add_row([ 'os', FormattedItem( @@ -95,8 +129,8 @@ def execute(client, args): if tag_row: t.add_row(['tags', listing(tag_row, separator=',')]) - ptr_domains = client['Hardware_Server'].\ - getReverseDomainRecords(id=hardware_id[1]) + ptr_domains = client['Hardware_Server'].getReverseDomainRecords( + id=hardware_id) for ptr_domain in ptr_domains: for ptr in ptr_domain['resourceRecords']: diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 5a5eef472..6fb27cce5 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -21,6 +21,7 @@ from DNS import DNSManager from CCI import CCIManager from metadata import MetadataManager +from hardware import HardwareManager from SoftLayer.exceptions import * # NOQA __title__ = 'SoftLayer' @@ -30,4 +31,4 @@ __copyright__ = 'Copyright 2013 SoftLayer Technologies, Inc.' __all__ = ['Client', 'BasicAuthentication', 'SoftLayerError', 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', - 'DNSManager', 'CCIManager', 'MetadataManager'] + 'DNSManager', 'CCIManager', 'MetadataManager', 'HardwareManager'] diff --git a/SoftLayer/hardware.py b/SoftLayer/hardware.py index 31ef8b31f..cff48eeab 100644 --- a/SoftLayer/hardware.py +++ b/SoftLayer/hardware.py @@ -1,7 +1,8 @@ -__all__ = ["HardwareManager"] +import socket +from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin -class HardwareManager(object): +class HardwareManager(IdentifierMixin, object): """ Manages hardware devices. """ def __init__(self, client): @@ -13,13 +14,41 @@ def __init__(self, client): self.client = client self.hardware = self.client['Hardware_Server'] self.account = self.client['Account'] + self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] - def list_hardware(self): + def list_hardware(self, hostname=None, public_ip=None, private_ip=None, + **kwargs): """ List all hardware. """ + if 'mask' not in kwargs: + items = set([ + 'id', + 'hostname', + 'globalIdentifier', + 'fullyQualifiedDomainName', + 'processorCoreAmount', + 'memoryCapacity', + 'primaryBackendIpAddress', + 'primaryIpAddress', + 'datacenter.name', + ]) + kwargs['mask'] = "mask[%s]" % ','.join(items) + + _filter = NestedDict(kwargs.get('filter') or {}) + if hostname: + _filter['hardware']['hostname'] = query_filter(hostname) + + if public_ip: + _filter['hardware']['primaryIpAddress'] = \ + query_filter(public_ip) + + if private_ip: + _filter['hardware']['primaryBackendIpAddress'] = \ + query_filter(private_ip) - return self.account.getHardware() + kwargs['filter'] = _filter.to_dict() + return self.account.getHardware(**kwargs) def get_hardware(self, id, **kwargs): """ Get details about a hardware device @@ -36,8 +65,10 @@ def get_hardware(self, id, **kwargs): 'hostname', 'domain', 'provisionDate', + 'hardwareStatus', + 'processorCoreAmount', + 'memoryCapacity', 'notes', - 'privateNetworkOnlyFlag', 'primaryBackendIpAddress', 'primaryIpAddress', 'datacenter.name', @@ -51,3 +82,23 @@ def get_hardware(self, id, **kwargs): kwargs['mask'] = "mask[%s]" % ','.join(items) return self.hardware.getObject(id=id, **kwargs) + + def _get_ids_from_hostname(self, hostname): + results = self.list_hardware(hostname=hostname, mask="id") + return [result['id'] for result in results] + + def _get_ids_from_ip(self, ip): + try: + # Does it look like an ip address? + socket.inet_aton(ip) + except socket.error: + return [] + + # Find the CCI via ip address. First try public ip, then private + results = self.list_hardware(public_ip=ip, mask="id") + if results: + return [result['id'] for result in results] + + results = self.list_hardware(private_ip=ip, mask="id") + if results: + return [result['id'] for result in results] diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/API/hardware_tests.py new file mode 100644 index 000000000..563f0a520 --- /dev/null +++ b/SoftLayer/tests/API/hardware_tests.py @@ -0,0 +1,72 @@ +""" + SoftLayer.tests.API.cci_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +import SoftLayer +import SoftLayer.hardware + +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +from mock import MagicMock, ANY, call + + +class HardwareTests_unittests(unittest.TestCase): + + def setUp(self): + self.client = MagicMock() + self.cci = SoftLayer.HardwareManager(self.client) + + def test_list_hardware(self): + mcall = call(mask=ANY, filter={}) + service = self.client.__getitem__() + + self.cci.list_hardware() + service.getHardware.assert_has_calls(mcall) + + def test_list_hardware_with_filters(self): + self.cci.list_hardware( + hostname='hostname', + public_ip='1.2.3.4', + private_ip='4.3.2.1', + ) + + service = self.client.__getitem__() + service.getHardware.assert_has_calls(call( + filter={ + 'hardware': { + 'hostname': {'operation': '_= hostname'}, + 'primaryIpAddress': {'operation': '_= 1.2.3.4'}, + 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'} + }}, + mask=ANY, + )) + + def test_resolve_ids_ip(self): + self.client.__getitem__().getHardware.return_value = [{'id': '1234'}] + _id = self.cci._get_ids_from_ip('1.2.3.4') + self.assertEqual(_id, ['1234']) + + self.client.__getitem__().getHardware.side_effect = \ + [[], [{'id': '4321'}]] + _id = self.cci._get_ids_from_ip('4.3.2.1') + self.assertEqual(_id, ['4321']) + + _id = self.cci._get_ids_from_ip('nope') + self.assertEqual(_id, []) + + def test_resolve_ids_hostname(self): + self.client.__getitem__().getHardware.return_value = [{'id': '1234'}] + _id = self.cci._get_ids_from_hostname('hostname') + self.assertEqual(_id, ['1234']) + + def test_get_instance(self): + self.client.__getitem__().getObject.return_value = { + 'hourlyVirtualGuests': "this is unique"} + self.cci.get_hardware(1) + self.client.__getitem__().getObject.assert_called_once_with( + id=1, mask=ANY) diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index b35fc7287..285472258 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -9,7 +9,7 @@ try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import patch @@ -119,6 +119,19 @@ def test_mb_to_gb(self): self.assertRaises(ValueError, cli.mb_to_gb, '1024string') + def test_gb(self): + item = cli.gb(2) + self.assertEqual(2, item.original) + self.assertEqual('2G', item.formatted) + + item = cli.gb('2') + self.assertEqual('2', item.original) + self.assertEqual('2G', item.formatted) + + item = cli.gb('2.0') + self.assertEqual('2.0', item.original) + self.assertEqual('2G', item.formatted) + def test_blank(self): item = cli.helpers.blank() self.assertEqual('NULL', item.original) From cc29c33e8eb619468881b5b204d08edcc9a35642 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 6 May 2013 11:21:27 -0500 Subject: [PATCH 0018/3227] Adds use of blank for hardware and CCI listing --- SoftLayer/CLI/modules/cci.py | 2 +- SoftLayer/CLI/modules/hardware.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index ec3b30d3a..7c532fa7e 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -110,7 +110,7 @@ def execute(client, args): for guest in guests: t.add_row([ guest['id'], - guest.get('datacenter', {}).get('name', 'unknown'), + guest.get('datacenter', {}).get('name', blank()), guest['fullyQualifiedDomainName'], guest['maxCpu'], mb_to_gb(guest['maxMemory']), diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 18858f037..1c3d096ae 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -55,7 +55,7 @@ def execute(client, args): for server in servers: t.add_row([ server['id'], - server.get('datacenter', {}).get('name', 'unknown'), + server.get('datacenter', {}).get('name', blank()), server['fullyQualifiedDomainName'], server['processorCoreAmount'], gb(server['memoryCapacity']), From e47ddbb23e852861eaa64f4fa028a09e15c948ec Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 8 May 2013 00:16:13 -0500 Subject: [PATCH 0019/3227] Polishes Hardware/CCI * Makes hardware details look more like CCI (ordering, naming convension, units) * Fixes several cases where blank() would never be presented when using NestedDict result.get('key', blank()) vs. result['key'] or blank() This should complete #54. --- SoftLayer/CLI/helpers.py | 3 ++- SoftLayer/CLI/modules/cci.py | 22 +++++++++++----------- SoftLayer/CLI/modules/hardware.py | 22 +++++++++++----------- SoftLayer/tests/CLI/helper_tests.py | 6 +++--- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index 47be55119..fa119d2da 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -34,7 +34,8 @@ def mb_to_gb(megabytes): def gb(gigabytes): - return FormattedItem(gigabytes, "%dG" % float(gigabytes)) + return FormattedItem(int(float(gigabytes)) * 1024, + "%dG" % int(float(gigabytes))) def blank(): diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 7c532fa7e..babd02e44 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -108,16 +108,17 @@ def execute(client, args): t.sortby = args.get('--sortby') or 'host' for guest in guests: + guest = NestedDict(guest) t.add_row([ guest['id'], - guest.get('datacenter', {}).get('name', blank()), + guest['datacenter']['name'] or blank(), guest['fullyQualifiedDomainName'], guest['maxCpu'], mb_to_gb(guest['maxMemory']), - guest.get('primaryIpAddress', blank()), - guest.get('primaryBackendIpAddress', blank()), - guest.get('activeTransaction', {}).get( - 'transactionStatus', {}).get('friendlyName', blank()), + guest['primaryIpAddress'] or blank(), + guest['primaryBackendIpAddress'] or blank(), + guest['activeTransaction']['transactionStatus'] + ['friendlyName'] or blank(), ]) return t @@ -151,19 +152,18 @@ def execute(client, args): 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'].get('name', blank())]) + t.add_row(['datacenter', result['datacenter']['name'] or blank()]) t.add_row(['cores', result['maxCpu']]) t.add_row(['memory', mb_to_gb(result['maxMemory'])]) - t.add_row(['public_ip', result.get('primaryIpAddress', blank())]) - t.add_row( - ['private_ip', result.get('primaryBackendIpAddress', blank())]) + t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) + t.add_row(['private_ip', result['primaryBackendIpAddress'] or blank()]) t.add_row([ 'os', FormattedItem( result['operatingSystem']['softwareLicense'] - ['softwareDescription'].get('referenceCode', blank()), + ['softwareDescription']['referenceCode'] or blank(), result['operatingSystem']['softwareLicense'] - ['softwareDescription'].get('name', blank()) + ['softwareDescription']['name'] or blank() )]) t.add_row(['private_only', result['privateNetworkOnlyFlag']]) t.add_row(['private_cpu', result['dedicatedAccountHostOnlyFlag']]) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 1c3d096ae..cd2d3fb2b 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -53,14 +53,15 @@ def execute(client, args): 'backend_ip' ]) for server in servers: + server = NestedDict(server) t.add_row([ server['id'], - server.get('datacenter', {}).get('name', blank()), + server['datacenter']['name'] or blank(), server['fullyQualifiedDomainName'], server['processorCoreAmount'], gb(server['memoryCapacity']), - server.get('primaryIpAddress', blank()), - server.get('primaryBackendIpAddress', blank()), + server['primaryIpAddress'] or blank(), + server['primaryBackendIpAddress'] or blank(), ]) return t @@ -93,22 +94,21 @@ 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'].get('name', blank())]) + t.add_row(['datacenter', result['datacenter']['name'] or blank()]) t.add_row(['cores', result['processorCoreAmount']]) - t.add_row(['memory', result['memoryCapacity']]) - t.add_row(['provisionDate', result['provisionDate']]) - t.add_row(['public_ip', result.get('primaryIpAddress', blank())]) + t.add_row(['memory', gb(result['memoryCapacity'])]) + t.add_row(['public_ip', result['primaryIpAddress'] or blank()]) t.add_row( - ['private_ip', result.get('primaryBackendIpAddress', blank())]) - + ['private_ip', result['primaryBackendIpAddress'] or blank()]) t.add_row([ 'os', FormattedItem( result['operatingSystem']['softwareLicense'] - ['softwareDescription'].get('referenceCode', blank()), + ['softwareDescription']['referenceCode'] or blank(), result['operatingSystem']['softwareLicense'] - ['softwareDescription'].get('name', blank()) + ['softwareDescription']['name'] or blank() )]) + t.add_row(['created', result['provisionDate']]) if result.get('notes'): t.add_row(['notes', result['notes']]) diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 285472258..65471bf5d 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -121,15 +121,15 @@ def test_mb_to_gb(self): def test_gb(self): item = cli.gb(2) - self.assertEqual(2, item.original) + self.assertEqual(2048, item.original) self.assertEqual('2G', item.formatted) item = cli.gb('2') - self.assertEqual('2', item.original) + self.assertEqual(2048, item.original) self.assertEqual('2G', item.formatted) item = cli.gb('2.0') - self.assertEqual('2.0', item.original) + self.assertEqual(2048, item.original) self.assertEqual('2G', item.formatted) def test_blank(self): From 3f7eb8342721d1944e64bf6cc1366f44d728f546 Mon Sep 17 00:00:00 2001 From: Brian Cline Date: Tue, 14 May 2013 17:00:36 -0500 Subject: [PATCH 0020/3227] Adds hardware reload support to API client and CLI. --- SoftLayer/CLI/modules/hardware.py | 24 +++++++++++++++++++++++- SoftLayer/hardware.py | 10 ++++++++++ SoftLayer/tests/API/hardware_tests.py | 5 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index cd2d3fb2b..4bb931508 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -7,12 +7,14 @@ The available commands are: list List hardware devices detail Retrieve hardware details + reload Perform an OS reload For several commands, will be asked for. This can be the id, hostname or the ip address for a piece of hardware. """ from SoftLayer.CLI.helpers import ( - CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, gb) + CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, + gb, no_going_back) from SoftLayer.hardware import HardwareManager @@ -137,3 +139,23 @@ def execute(client, args): t.add_row(['ptr', ptr['data']]) return t + + +class HardwareReload(CLIRunnable): + """ +usage: sl hardware reload [options] + +Reload the OS on a hardware server based on its current configuration +""" + + action = 'reload' + options = ['confirm'] + + @staticmethod + def execute(client, args): + hardware = HardwareManager(client) + hardware_id = resolve_id(hardware, args.get('')) + if args['--really'] or no_going_back(hardware_id): + hardware.reload(hardware_id) + else: + CLIAbort('Aborted') diff --git a/SoftLayer/hardware.py b/SoftLayer/hardware.py index cff48eeab..d7dc3bf72 100644 --- a/SoftLayer/hardware.py +++ b/SoftLayer/hardware.py @@ -83,6 +83,16 @@ def get_hardware(self, id, **kwargs): return self.hardware.getObject(id=id, **kwargs) + def reload(self, id): + """ Perform an OS reload of a server with its current configuration. + + :param integer id: the instance ID to reload + + """ + + return self.hardware.reloadCurrentOperatingSystemConfiguration(id=id, + token='FORCE') + def _get_ids_from_hostname(self, hostname): results = self.list_hardware(hostname=hostname, mask="id") return [result['id'] for result in results] diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/API/hardware_tests.py index 563f0a520..426c558f7 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/API/hardware_tests.py @@ -70,3 +70,8 @@ def test_get_instance(self): self.cci.get_hardware(1) self.client.__getitem__().getObject.assert_called_once_with( id=1, mask=ANY) + + def test_reload(self): + self.cci.reload(id=1) + f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration + f.assert_called_once_with(id=1, token='FORCE') From 980b25dd527288d0ac4d5efc650ba772c974cfb2 Mon Sep 17 00:00:00 2001 From: Brian Cline Date: Tue, 14 May 2013 17:14:48 -0500 Subject: [PATCH 0021/3227] Fixes name of hardware manager in tests --- SoftLayer/tests/API/hardware_tests.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/API/hardware_tests.py index 426c558f7..defd8680a 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/API/hardware_tests.py @@ -1,5 +1,5 @@ """ - SoftLayer.tests.API.cci_tests + SoftLayer.tests.API.hardware_tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. @@ -19,17 +19,17 @@ class HardwareTests_unittests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.cci = SoftLayer.HardwareManager(self.client) + self.hardware = SoftLayer.HardwareManager(self.client) def test_list_hardware(self): mcall = call(mask=ANY, filter={}) service = self.client.__getitem__() - self.cci.list_hardware() + self.hardware.list_hardware() service.getHardware.assert_has_calls(mcall) def test_list_hardware_with_filters(self): - self.cci.list_hardware( + self.hardware.list_hardware( hostname='hostname', public_ip='1.2.3.4', private_ip='4.3.2.1', @@ -48,30 +48,30 @@ def test_list_hardware_with_filters(self): def test_resolve_ids_ip(self): self.client.__getitem__().getHardware.return_value = [{'id': '1234'}] - _id = self.cci._get_ids_from_ip('1.2.3.4') + _id = self.hardware._get_ids_from_ip('1.2.3.4') self.assertEqual(_id, ['1234']) self.client.__getitem__().getHardware.side_effect = \ [[], [{'id': '4321'}]] - _id = self.cci._get_ids_from_ip('4.3.2.1') + _id = self.hardware._get_ids_from_ip('4.3.2.1') self.assertEqual(_id, ['4321']) - _id = self.cci._get_ids_from_ip('nope') + _id = self.hardware._get_ids_from_ip('nope') self.assertEqual(_id, []) def test_resolve_ids_hostname(self): self.client.__getitem__().getHardware.return_value = [{'id': '1234'}] - _id = self.cci._get_ids_from_hostname('hostname') + _id = self.hardware._get_ids_from_hostname('hostname') self.assertEqual(_id, ['1234']) def test_get_instance(self): self.client.__getitem__().getObject.return_value = { 'hourlyVirtualGuests': "this is unique"} - self.cci.get_hardware(1) + self.hardware.get_hardware(1) self.client.__getitem__().getObject.assert_called_once_with( id=1, mask=ANY) def test_reload(self): - self.cci.reload(id=1) + self.hardware.reload(id=1) f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration f.assert_called_once_with(id=1, token='FORCE') From d474e0adf542e7797fbfb6a79f09db659cfd3c9d Mon Sep 17 00:00:00 2001 From: Brian Cline Date: Tue, 14 May 2013 17:26:49 -0500 Subject: [PATCH 0022/3227] Macaroni called and asked for some tildes --- SoftLayer/tests/API/hardware_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/API/hardware_tests.py index defd8680a..5e8a62884 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/API/hardware_tests.py @@ -1,6 +1,6 @@ """ SoftLayer.tests.API.hardware_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. From 5b968ff4ca99fc42a9cca3062ec6065b4635bee7 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 16 May 2013 14:56:02 -0500 Subject: [PATCH 0023/3227] Gives a more informative error message for invalid arguments. --- SoftLayer/CLI/core.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 2626ecc9f..f12fdf9e8 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -30,7 +30,7 @@ import os.path from prettytable import FRAME, NONE -from docopt import docopt +from docopt import docopt, DocoptExit from SoftLayer import Client, SoftLayerError from SoftLayer.consts import VERSION @@ -200,6 +200,11 @@ def main(args=sys.argv[1:], env=Environment()): except (ValueError, KeyError): raise + except DocoptExit, e: + env.err(e.message) + env.err( + '\nUnknown argument(s), use -h or --help for available options') + exit_status = 127 except KeyboardInterrupt: env.out('') exit_status = 1 From b5948459a2467b8c69db393bc4084149ba22f851 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 May 2013 14:02:41 -0500 Subject: [PATCH 0024/3227] Refactor of CLI core. Refactors core CLI code in order to make help a first-class module and give more verbose error messages when the module or command is not found. Example: > sl help > sl help cci > sl help cci list > sl invalid Invalid module: "invalid". > sl cci invalid Invalid command: "invalid". --- SoftLayer/CLI/core.py | 157 +++++++++++++---------- SoftLayer/CLI/environment.py | 37 +++++- SoftLayer/CLI/modules/help.py | 28 ++++ SoftLayer/tests/CLI/core_tests.py | 72 ++++++++--- SoftLayer/tests/CLI/environment_tests.py | 25 +++- 5 files changed, 225 insertions(+), 94 deletions(-) create mode 100644 SoftLayer/CLI/modules/help.py diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index f12fdf9e8..abb564b70 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -1,23 +1,25 @@ """ -usage: sl [...] - sl help +usage: sl [...] + sl help + sl help sl [-h | --help] SoftLayer Command-line Client -The available commands are: +The available modules are: cci Manage, delete, order compute instances config View and edit configuration for this tool dns Manage DNS firewall Firewall rule and security management hardware View hardware details + 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 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' @@ -36,7 +38,8 @@ from SoftLayer.consts import VERSION from SoftLayer.CLI.helpers import ( Table, CLIAbort, FormattedItem, listing, ArgumentError, SequentialOutput) -from SoftLayer.CLI.environment import Environment, CLIRunnableType +from SoftLayer.CLI.environment import ( + Environment, CLIRunnableType, InvalidCommand, InvalidModule) def format_output(data, fmt='table'): @@ -89,98 +92,104 @@ def format_no_tty(table): return t -def parse_main_args(args=sys.argv[1:]): - arguments = docopt(__doc__, version=VERSION, argv=args, options_first=True) - return arguments +class CommandParser(object): + def __init__(self, env): + self.env = env + def get_main_help(self): + return __doc__.strip() -def parse_module_args(module, args): - - arg_doc = module.__doc__ + """ + def get_module_help(self, module_name): + module = self.env.load_module(module_name) + arg_doc = module.__doc__ + """ Standard Options: -h --help Show this screen """ - arguments = docopt( - arg_doc, version=VERSION, argv=args, options_first=True) - return arguments + return arg_doc.strip() + def get_command_help(self, module_name, command_name): + command = self.env.get_command(module_name, command_name) -def parse_submodule_args(submodule, args): - default_format = 'raw' - if sys.stdout.isatty(): - default_format = 'table' + default_format = 'raw' + if sys.stdout.isatty(): + default_format = 'table' - arg_doc = submodule.__doc__ + arg_doc = command.__doc__ - if 'confirm' in submodule.options: - arg_doc += """ + if 'confirm' in command.options: + arg_doc += """ Prompt Options: -y, --really Confirm all prompt actions """ - if '[options]' in arg_doc: - arg_doc += """ + if '[options]' in arg_doc: + arg_doc += """ Standard Options: --format=ARG Output format. [Options: table, raw] [Default: %s] -C FILE --config=FILE Config file location. [Default: ~/.softlayer] -h --help Show this screen """ % default_format + return arg_doc.strip() + + def parse_main_args(self, args): + main_help = self.get_main_help() + arguments = docopt( + main_help, + version=VERSION, + argv=args, + options_first=True) + arguments[''] = self.env.get_module_name(arguments['']) + return arguments + + def parse_module_args(self, module_name, args): + arg_doc = self.get_module_help(module_name) + arguments = docopt( + arg_doc, + version=VERSION, + argv=[module_name] + args, + options_first=True) + module = self.env.load_module(module_name) + return module, arguments + + def parse_command_args(self, module_name, command_name, args): + command = self.env.get_command(module_name, command_name) + arg_doc = self.get_command_help(module_name, command_name) + arguments = docopt(arg_doc, version=VERSION, argv=[module_name] + args) + return command, arguments + + def parse(self, args): + # handle `sl ...` + main_args = self.parse_main_args(args) + module_name = main_args[''] + + # handle `sl ...` + module, module_args = self.parse_module_args( + module_name, main_args['']) + command_name = module_args[''] - arguments = docopt(arg_doc, version=VERSION, argv=args) - return arguments + # handle `sl ...` + return self.parse_command_args( + module_name, + command_name, + main_args['']) def main(args=sys.argv[1:], env=Environment()): """ - Handle conditions in this order: - - sl [help] [(-h | --help)] -> show main help - sl help -> show command-specific help - sl [(-h | --help)] -> show command-specific help - sl invalid_command -> show main help - sl (-h | --help) -> show subcommand-specific help - sl [options] -> execute subcommand + Entry point for the command-line client. """ # Parse Top-Level Arguments CLIRunnableType.env = env exit_status = 0 + resolver = CommandParser(env) try: - # handle `sl ...` - main_args = parse_main_args(args) - module_name = env.get_module_name(main_args['']) - - # handle `sl help ` - if module_name == 'help' and len(main_args['']) > 0: - module = env.load_module(main_args[''][0]) - parse_module_args(module, ['--help', main_args[''][0]]) - - # handle `sl --help` and `sl invalidcommand` - if module_name not in env.plugin_list(): - parse_main_args(['--help']) - - module = env.load_module(module_name) - actions = env.plugins.get(module_name) or [] - - # handle `sl ...` - module_args = parse_module_args( - module, [module_name] + main_args['']) - action_name = module_args[''] - - # handle `sl invalidcommand` - if action_name not in actions: - parse_module_args(module, ['--help', module_name, action_name]) - - action = actions[action_name] - - # handle `sl ...` - submodule_args = parse_submodule_args( - action, [module_name] + main_args['']) + command, command_args = resolver.parse(args) # Parse Config config_files = ["~/.softlayer"] - if submodule_args.get('--config'): - config_files.append(submodule_args.get('--config')) + if command_args.get('--config'): + config_files.append(command_args.get('--config')) env.load_config(config_files) client = Client( @@ -189,15 +198,27 @@ def main(args=sys.argv[1:], env=Environment()): endpoint_url=env.config.get('endpoint_url')) # Do the thing - data = action.execute(client, submodule_args) + data = command.execute(client, command_args) if data: - format = submodule_args.get('--format', 'table') + format = command_args.get('--format', 'table') if format not in ['raw', 'table']: - raise ArgumentError('Invalid Format "%s"' % format) + raise ArgumentError('Invalid format "%s"' % format) s = format_output(data, fmt=format) if s: env.out(s) + except InvalidCommand, e: + env.err(resolver.get_module_help(e.module_name)) + if e.command_name: + env.err('') + env.err(str(e)) + exit_status = 1 + except InvalidModule, e: + env.err(resolver.get_main_help()) + if e.module_name: + env.err('') + env.err(str(e)) + exit_status = 1 except (ValueError, KeyError): raise except DocoptExit, e: diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 8d3bcecca..6e6f17d8b 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -13,7 +13,24 @@ import os.path from SoftLayer.CLI.modules import get_module_list -from SoftLayer import API_PUBLIC_ENDPOINT +from SoftLayer import API_PUBLIC_ENDPOINT, SoftLayerError + + +class InvalidCommand(SoftLayerError): + " Raised when trying to use a command that does not exist " + def __init__(self, module_name, command_name, *args): + self.module_name = module_name + self.command_name = command_name + error = 'Invalid command: "%s".' % self.command_name + SoftLayerError.__init__(self, error, *args) + + +class InvalidModule(SoftLayerError): + " Raised when trying to use a module that does not exist " + def __init__(self, module_name, *args): + self.module_name = module_name + error = 'Invalid module: "%s".' % self.module_name + SoftLayerError.__init__(self, error, *args) class Environment(object): @@ -27,13 +44,24 @@ class Environment(object): stdout = sys.stdout stderr = sys.stderr + def get_command(self, module_name, command_name): + actions = self.plugins.get(module_name) or {} + if command_name in actions: + return actions[command_name] + if None in actions: + return actions[None] + raise InvalidCommand(module_name, command_name) + def get_module_name(self, module_name): if module_name in self.aliases: return self.aliases[module_name] return module_name - def load_module(self, mod): # pragma: no cover - return import_module('SoftLayer.CLI.modules.%s' % mod) + def load_module(self, module_name): # pragma: no cover + try: + return import_module('SoftLayer.CLI.modules.%s' % module_name) + except ImportError: + raise InvalidModule(module_name) def add_plugin(self, cls): command = cls.__module__.split('.')[-1] @@ -76,6 +104,9 @@ def load_config(self, files): self.config = config + def exit(self, code=0): + sys.exit(code) + class CLIRunnableType(type): diff --git a/SoftLayer/CLI/modules/help.py b/SoftLayer/CLI/modules/help.py new file mode 100644 index 000000000..1f5101f75 --- /dev/null +++ b/SoftLayer/CLI/modules/help.py @@ -0,0 +1,28 @@ +""" +usage: sl help [options] + sl help [options] + sl help [options] + +View help on a module or command. +""" +# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. +# :license: BSD, see LICENSE for more details. + +from SoftLayer.CLI.core import CommandParser +from SoftLayer.CLI import CLIRunnable + + +class Show(CLIRunnable): + # Use the same documentation as the module + __doc__ = __doc__ + action = None + + @classmethod + def execute(cls, client, args): + parser = CommandParser(cls.env) + cls.env.load_module(args['']) + if args['']: + return parser.get_command_help(args[''], args['']) + elif args['']) + return parser.get_main_help() diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index ccd26f010..d2c111843 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -16,6 +16,7 @@ import SoftLayer import SoftLayer.CLI as cli from SoftLayer.CLI.helpers import CLIAbort +from SoftLayer.CLI.environment import Environment, InvalidModule def module_fixture(): @@ -39,12 +40,25 @@ def execute(client, args): return "test" +class EnvironmentFixture(Environment): + plugins = {'cci': {'list': submodule_fixture}} + aliases = { + 'meta': 'metadata', + 'my': 'metadata', + } + config = {} + + def load_module(self, *args, **kwargs): + return module_fixture + + def plugin_list(self, *args, **kwargs): + return self.plugins.keys() + + class CommandLineTests(unittest.TestCase): def setUp(self): - self.env = MagicMock() - self.env.plugin_list.return_value = ['cci'] - self.env.plugins = {'cci': {'list': submodule_fixture}} - self.env.load_module.return_value = module_fixture + self.env = EnvironmentFixture() + self.env.get_module_name = MagicMock() def test_normal_path(self): self.env.get_module_name.return_value = 'cci' @@ -80,11 +94,21 @@ def test_abort(self): self.assertRaises( SystemExit, cli.core.main, args=['cci', 'list'], env=self.env) + def test_invalid_module_error(self): + self.env.get_module_name.side_effect = InvalidModule('cci') + self.assertRaises( + SystemExit, cli.core.main, args=['cci', 'list'], env=self.env) + def test_softlayer_error(self): self.env.get_module_name.side_effect = SoftLayer.SoftLayerError 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( + 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( @@ -95,59 +119,67 @@ def test_value_key_errors(self): KeyError, cli.core.main, args=['cci', 'list'], env=self.env) -class TestParseMainArgs(unittest.TestCase): +class TestCommandParser(unittest.TestCase): + def setUp(self): + self.env = EnvironmentFixture() + self.parser = cli.core.CommandParser(self.env) + def test_main(self,): - args = cli.core.parse_main_args( + args = self.parser.parse_main_args( args=['cci', 'list']) + print args self.assertEqual(args['help'], False) - self.assertEqual(args[''], 'cci') + self.assertEqual(args[''], 'cci') self.assertEqual(args[''], ['list']) def test_primary_help(self): - args = cli.core.parse_main_args(args=[]) + args = self.parser.parse_main_args(args=[]) self.assertEqual({ '--help': False, '-h': False, '': [], + '': None, '': None, 'help': False, }, args) - args = cli.core.parse_main_args(args=['help']) + args = self.parser.parse_main_args(args=['help']) self.assertEqual({ '--help': False, '-h': False, '': [], - '': 'help', + '': 'help', + '': None, 'help': False, }, args) - args = cli.core.parse_main_args(args=['help', 'module']) + args = self.parser.parse_main_args(args=['help', 'module']) self.assertEqual({ '--help': False, '-h': False, '': ['module'], - '': 'help', + '': 'help', + '': None, 'help': False, }, args) self.assertRaises( - SystemExit, cli.core.parse_main_args, args=['--help']) - + SystemExit, self.parser.parse_main_args, args=['--help']) -class TestParseSubmoduleArgs(unittest.TestCase): @patch('sys.stdout.isatty', return_value=True) def test_tty(self, tty): self.assertRaises( - SystemExit, cli.core.parse_submodule_args, submodule_fixture, []) + SystemExit, self.parser.parse_command_args, 'cci', 'list', []) def test_confirm(self): - submodule = MagicMock() - submodule.options = ['confirm'] - submodule.__doc__ = 'usage: sl cci list [options]' + command = MagicMock() + command.options = ['confirm'] + command.__doc__ = 'usage: sl cci list [options]' + self.env.get_command = MagicMock() + self.env.get_command.return_value = command self.assertRaises( - SystemExit, cli.core.parse_submodule_args, submodule, ['']) + SystemExit, self.parser.parse_command_args, 'cci', 'list', []) class TestFormatOutput(unittest.TestCase): diff --git a/SoftLayer/tests/CLI/environment_tests.py b/SoftLayer/tests/CLI/environment_tests.py index d5b8f4ab0..109849929 100644 --- a/SoftLayer/tests/CLI/environment_tests.py +++ b/SoftLayer/tests/CLI/environment_tests.py @@ -10,11 +10,11 @@ try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import patch, MagicMock from SoftLayer import API_PUBLIC_ENDPOINT -import SoftLayer.CLI as cli +from SoftLayer.CLI.environment import Environment, InvalidCommand if sys.version_info >= (3,): raw_input_path = 'builtins.input' @@ -26,7 +26,7 @@ class EnvironmentTests(unittest.TestCase): def setUp(self): - self.env = cli.environment.Environment() + self.env = Environment() def test_plugin_list(self): actions = self.env.plugin_list() @@ -93,3 +93,22 @@ def test_get_module_name(self): r = self.env.get_module_name('realname') self.assertEqual(r, 'realname') + + def test_get_command_invalid(self): + self.assertRaises(InvalidCommand, self.env.get_command, 'cci', 'list') + + def test_get_command(self): + self.env.plugins = {'cci': {'list': 'something'}} + command = self.env.get_command('cci', 'list') + self.assertEqual(command, 'something') + + def test_get_command_none(self): + # If None is in the action list, anything that doesn't exist as a + # command will return the value of the None key. This is to support + # sl help any_module_name + self.env.plugins = {'cci': {None: 'something'}} + command = self.env.get_command('cci', 'something else') + self.assertEqual(command, 'something') + + def test_exit(self): + self.assertRaises(SystemExit, self.env.exit, 1) From fd47ad9a6a69a42f5ee8f16e02333045d6cccab6 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 May 2013 14:22:12 -0500 Subject: [PATCH 0025/3227] Fixes bug on Python 3 because SystemExit does not have a message attribute. --- SoftLayer/CLI/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index abb564b70..88bc4b7b0 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -222,7 +222,7 @@ def main(args=sys.argv[1:], env=Environment()): except (ValueError, KeyError): raise except DocoptExit, e: - env.err(e.message) + env.err(e.usage) env.err( '\nUnknown argument(s), use -h or --help for available options') exit_status = 127 From 790670aca6829cf3b7d2d5014e8e65919d2b9366 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 May 2013 15:20:01 -0500 Subject: [PATCH 0026/3227] TRIVIAL: removes needless debugging in a test --- SoftLayer/tests/CLI/core_tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index d2c111843..4fe6b4bb0 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -127,7 +127,6 @@ def setUp(self): def test_main(self,): args = self.parser.parse_main_args( args=['cci', 'list']) - print args self.assertEqual(args['help'], False) self.assertEqual(args[''], 'cci') From d4c8f77533093c6e78c0bdfeb2bac828e8309cb3 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 May 2013 17:03:04 -0500 Subject: [PATCH 0027/3227] Fixes Typo --- SoftLayer/CLI/modules/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/help.py b/SoftLayer/CLI/modules/help.py index 1f5101f75..9be1e53b0 100644 --- a/SoftLayer/CLI/modules/help.py +++ b/SoftLayer/CLI/modules/help.py @@ -23,6 +23,6 @@ def execute(cls, client, args): cls.env.load_module(args['']) if args['']: return parser.get_command_help(args[''], args['']) - elif args['']: return parser.get_module_help(args['']) return parser.get_main_help() From 04b58ce3084f2b538cad29d7824e7ddd8ac953e9 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 May 2013 17:53:56 -0500 Subject: [PATCH 0028/3227] Adds some hardware filters --- SoftLayer/CLI/modules/filters.py | 4 ++-- SoftLayer/CLI/modules/hardware.py | 32 ++++++++++++++++++++++++- SoftLayer/hardware.py | 34 ++++++++++++++++++++++++--- SoftLayer/tests/API/hardware_tests.py | 19 ++++++++++++--- 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/SoftLayer/CLI/modules/filters.py b/SoftLayer/CLI/modules/filters.py index 1e4274ac0..e2fde7000 100644 --- a/SoftLayer/CLI/modules/filters.py +++ b/SoftLayer/CLI/modules/filters.py @@ -20,8 +20,8 @@ '<= value' Less than or equal to value Examples: - sl cci list --datacenter=dal05 - sl cci list --hostname='prod*' + sl hardware list --datacenter=dal05 + sl hardware list --hostname='prod*' sl cci list --network=100 --cpu=2 sl cci list --network='< 100' --cpu=2 sl cci list --memory='>= 2048' diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 4bb931508..61dfd3b2a 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -36,6 +36,24 @@ class ListHardware(CLIRunnable): """ usage: sl hardware list [options] +Examples: + 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, + memory, primary_ip, backend_ip + +Filters: + -H --hostname=HOST Host portion of the FQDN. example: server + -D --domain=DOMAIN Domain portion of the FQDN. example: example.com + -d DC, --datacenter=DC datacenter shortname (sng01, dal05, ...) + -n MBPS, --network=MBPS Network port speed in Mbps + --tags=ARG Only show instances that have one of these tags. + Comma-separated. (production,db) + +For more on filters see 'sl help filters' List hardware servers on the acount """ action = 'list' @@ -44,7 +62,17 @@ class ListHardware(CLIRunnable): def execute(client, args): manager = HardwareManager(client) - servers = manager.list_hardware() + tags = None + if args.get('--tags'): + tags = [tag.strip() for tag in args.get('--tags').split(',')] + + servers = manager.list_hardware( + hostname=args.get('--hostname'), + domain=args.get('--domain'), + datacenter=args.get('--datacenter'), + nic_speed=args.get('--network'), + tags=tags) + t = Table([ 'id', 'datacenter', @@ -54,6 +82,8 @@ def execute(client, args): 'primary_ip', 'backend_ip' ]) + t.sortby = args.get('--sortby') or 'host' + for server in servers: server = NestedDict(server) t.add_row([ diff --git a/SoftLayer/hardware.py b/SoftLayer/hardware.py index d7dc3bf72..acdb8b58e 100644 --- a/SoftLayer/hardware.py +++ b/SoftLayer/hardware.py @@ -16,10 +16,20 @@ def __init__(self, client): self.account = self.client['Account'] self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] - def list_hardware(self, hostname=None, public_ip=None, private_ip=None, - **kwargs): + def list_hardware(self, tags=None, hostname=None, domain=None, + datacenter=None, nic_speed=None, public_ip=None, + private_ip=None, **kwargs): """ List all hardware. + :param list tags: filter based on tags + :param string hostname: filter based on hostname + :param string domain: filter based on domain + :param string datacenter: filter based on datacenter + :param integer nic_speed: filter based on network speed (in MBPS) + :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.) + """ if 'mask' not in kwargs: items = set([ @@ -36,9 +46,26 @@ def list_hardware(self, hostname=None, public_ip=None, private_ip=None, kwargs['mask'] = "mask[%s]" % ','.join(items) _filter = NestedDict(kwargs.get('filter') or {}) + if tags: + _filter['hardware']['tagReferences']['tag']['name'] = { + 'operation': 'in', + 'options': [{'name': 'data', 'value': tags}], + } + if hostname: _filter['hardware']['hostname'] = query_filter(hostname) + if domain: + _filter['hardware']['domain'] = query_filter(domain) + + if datacenter: + _filter['hardware']['datacenter']['name'] = \ + query_filter(datacenter) + + if nic_speed: + _filter['hardware']['networkComponents']['maxSpeed'] = \ + query_filter(nic_speed) + if public_ip: _filter['hardware']['primaryIpAddress'] = \ query_filter(public_ip) @@ -90,7 +117,8 @@ def reload(self, id): """ - return self.hardware.reloadCurrentOperatingSystemConfiguration(id=id, + return self.hardware.reloadCurrentOperatingSystemConfiguration( + id=id, token='FORCE') def _get_ids_from_hostname(self, hostname): diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/API/hardware_tests.py index 5e8a62884..e415ec218 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/API/hardware_tests.py @@ -30,19 +30,32 @@ def test_list_hardware(self): def test_list_hardware_with_filters(self): self.hardware.list_hardware( + tags=['tag1', 'tag2'], hostname='hostname', + domain='example.com', + datacenter='dal05', + nic_speed=100, public_ip='1.2.3.4', private_ip='4.3.2.1', ) - service = self.client.__getitem__() service.getHardware.assert_has_calls(call( filter={ 'hardware': { + 'datacenter': {'name': {'operation': '_= dal05'}}, + 'domain': {'operation': '_= example.com'}, + 'tagReferences': { + 'tag': {'name': { + 'operation': 'in', + 'options': [ + {'name': 'data', 'value': ['tag1', 'tag2']}] + }} + }, 'hostname': {'operation': '_= hostname'}, 'primaryIpAddress': {'operation': '_= 1.2.3.4'}, - 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'} - }}, + 'networkComponents': {'maxSpeed': {'operation': 100}}, + 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'}} + }, mask=ANY, )) From b721cb01f1d0c834066399a85c745707bdae7bba Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 17 May 2013 17:59:44 -0500 Subject: [PATCH 0029/3227] Moves hardware list description test to the right place --- SoftLayer/CLI/modules/hardware.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 61dfd3b2a..efadbbabc 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -36,6 +36,8 @@ class ListHardware(CLIRunnable): """ usage: sl hardware list [options] +List hardware servers on the acount + Examples: sl hardware list --datacenter=dal05 sl hardware list --network=100 --domain=example.com @@ -54,7 +56,6 @@ class ListHardware(CLIRunnable): Comma-separated. (production,db) For more on filters see 'sl help filters' -List hardware servers on the acount """ action = 'list' From 73792c0ec6d06ed8452fbef564afba0af834e004 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Sun, 19 May 2013 17:25:31 -0500 Subject: [PATCH 0030/3227] Removes support for Python 2.5 * Adds fallback to distutils if setuptools is not installed * Removes the xmlrpc transport and moved the requests transport to SoftLayer/transport.py * Adds make_rest_api_call to SoftLayer.transport and related tests. SoftLayer.metadata is currently the only thing that uses it. I think both of the transport types should have the same interface, but that's not the case at the moment. * Removes requirement for simplejson if python version is less than 2.6. * setup.py bails out early if the python version is less than 2. --- .travis.yml | 3 +- SoftLayer/API.py | 10 ++- SoftLayer/metadata.py | 37 +++------ SoftLayer/tests/API/client_tests.py | 42 +++++----- SoftLayer/tests/API/functional_tests.py | 39 +-------- SoftLayer/tests/API/metadata_tests.py | 40 ++++----- SoftLayer/tests/API/transport_tests.py | 75 +++++++++++++++++ SoftLayer/tests/CLI/helper_tests.py | 1 - .../requests_transport.py => transport.py} | 45 +++++++++-- SoftLayer/transport/__init__.py | 4 - SoftLayer/transport/xmlrpclib_transport.py | 81 ------------------- setup.py | 32 +++++--- tox.ini | 2 +- 13 files changed, 200 insertions(+), 211 deletions(-) rename SoftLayer/{transport/requests_transport.py => transport.py} (54%) delete mode 100644 SoftLayer/transport/__init__.py delete mode 100644 SoftLayer/transport/xmlrpclib_transport.py diff --git a/.travis.yml b/.travis.yml index 9ff7115c1..ff4a0f909 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.5" - "2.6" - "2.7" - "3.2" @@ -9,7 +8,7 @@ python: # command to install dependencies install: - "pip install -r requirements.txt --use-mirrors" - - "if [[ $TRAVIS_PYTHON_VERSION = 2.6 || $TRAVIS_PYTHON_VERSION = 2.5 ]]; then pip install unittest2 --use-mirrors; fi" + - "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/SoftLayer/API.py b/SoftLayer/API.py index b6b6d2b64..ac1cfa482 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -8,7 +8,7 @@ """ from SoftLayer.consts import API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT, \ USER_AGENT -from SoftLayer.transport import make_api_call +from SoftLayer.transport import make_xml_rpc_api_call from SoftLayer.exceptions import SoftLayerError import os @@ -281,9 +281,11 @@ def call(self, service, method, *args, **kwargs): 'offset': int(offset) } uri = '/'.join([self._endpoint_url, service]) - return make_api_call(uri, method, args, headers=headers, - http_headers=http_headers, timeout=self.timeout, - verbose=self.verbose) + return make_xml_rpc_api_call(uri, method, args, + headers=headers, + http_headers=http_headers, + timeout=self.timeout, + verbose=self.verbose) __call__ = call diff --git a/SoftLayer/metadata.py b/SoftLayer/metadata.py index faede438f..3f11e9d27 100644 --- a/SoftLayer/metadata.py +++ b/SoftLayer/metadata.py @@ -6,12 +6,9 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -try: - import simplejson as json -except ImportError: # pragma: no cover - import json # NOQA -import urllib2 +import json +from SoftLayer.transport import make_rest_api_call from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST, USER_AGENT from SoftLayer.exceptions import SoftLayerAPIError, SoftLayerError @@ -64,26 +61,14 @@ def __init__(self, client=None, timeout=5): def make_request(self, path): url = '/'.join([self.url, 'SoftLayer_Resource_Metadata', path]) - req = urllib2.Request(url) - req.add_header('User-Agent', USER_AGENT) - try: - resp = urllib2.urlopen(req, timeout=self.timeout) - except urllib2.HTTPError, e: # pragma: no cover - if e.code == 404: + return make_rest_api_call('GET', url, + http_headers={'User-Agent': USER_AGENT}, + timeout=self.timeout) + except SoftLayerAPIError, e: + if e.faultCode == 404: return None - - try: - content = json.loads(e.read()) - raise SoftLayerAPIError(content['code'], content['error']) - except (ValueError, KeyError): - pass - - raise SoftLayerAPIError(e.code, e.reason) - except urllib2.URLError, e: - raise SoftLayerAPIError(0, e.reason) - else: - return resp.read() + raise e def get(self, name, param=None): """ Retreive a metadata attribute @@ -104,11 +89,11 @@ def get(self, name, param=None): if not param: raise SoftLayerError( 'Parameter required to get this attribute.') - url = "%s/%s%s" % (self.attribs[name]['call'], param, extension) + path = "%s/%s%s" % (self.attribs[name]['call'], param, extension) else: - url = "%s%s" % (self.attribs[name]['call'], extension) + path = "%s%s" % (self.attribs[name]['call'], extension) - data = self.make_request(url) + data = self.make_request(path) if data and extension == '.json': return json.loads(data) return data diff --git a/SoftLayer/tests/API/client_tests.py b/SoftLayer/tests/API/client_tests.py index dc4cfbe22..5c37f2dd6 100644 --- a/SoftLayer/tests/API/client_tests.py +++ b/SoftLayer/tests/API/client_tests.py @@ -8,7 +8,7 @@ try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import patch, MagicMock, call @@ -118,15 +118,15 @@ def setUp(self): username='doesnotexist', api_key='issurelywrong', endpoint_url="ENDPOINT") - @patch('SoftLayer.API.make_api_call') - def test_old_api(self, make_api_call): + @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_api_call.assert_called_with( + make_xml_rpc_api_call.assert_called_with( 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), headers={ 'authenticate': { @@ -138,8 +138,8 @@ def test_old_api(self, make_api_call): 'User-Agent': USER_AGENT, }) - @patch('SoftLayer.API.make_api_call') - def test_complex_old_api(self, make_api_call): + @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") @@ -156,7 +156,7 @@ def test_complex_old_api(self, make_api_call): 'TYPE': {'obj': {'attribute': {'operation': '^= prefix'}}}}, limit=9, offset=10) - make_api_call.assert_called_with( + make_xml_rpc_api_call.assert_called_with( 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (1234, ), headers={ 'SoftLayer_SERVICEObjectMask': { @@ -181,10 +181,10 @@ def test_old_api_no_service(self): api_key='issurelywrong') self.assertRaises(SoftLayer.SoftLayerError, client.METHOD) - @patch('SoftLayer.API.make_api_call') - def test_simple_call(self, make_api_call): + @patch('SoftLayer.API.make_xml_rpc_api_call') + def test_simple_call(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD() - make_api_call.assert_called_with( + make_xml_rpc_api_call.assert_called_with( 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), headers={ 'authenticate': { @@ -196,8 +196,8 @@ def test_simple_call(self, make_api_call): 'User-Agent': USER_AGENT, }) - @patch('SoftLayer.API.make_api_call') - def test_complex(self, make_api_call): + @patch('SoftLayer.API.make_xml_rpc_api_call') + def test_complex(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD( 1234, id=5678, @@ -207,7 +207,7 @@ def test_complex(self, make_api_call): 'TYPE': {'obj': {'attribute': {'operation': '^= prefix'}}}}, limit=9, offset=10) - make_api_call.assert_called_with( + make_xml_rpc_api_call.assert_called_with( 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (1234, ), headers={ 'SoftLayer_SERVICEObjectMask': { @@ -227,11 +227,11 @@ def test_complex(self, make_api_call): 'User-Agent': USER_AGENT, }) - @patch('SoftLayer.API.make_api_call') - def test_mask_call_v2(self, make_api_call): + @patch('SoftLayer.API.make_xml_rpc_api_call') + def test_mask_call_v2(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD( mask="mask[something[nested]]") - make_api_call.assert_called_with( + make_xml_rpc_api_call.assert_called_with( 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), headers={ 'authenticate': { @@ -244,11 +244,11 @@ def test_mask_call_v2(self, make_api_call): 'User-Agent': USER_AGENT, }) - @patch('SoftLayer.API.make_api_call') - def test_mask_call_v2_dot(self, make_api_call): + @patch('SoftLayer.API.make_xml_rpc_api_call') + def test_mask_call_v2_dot(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD( mask="mask.something.nested") - make_api_call.assert_called_with( + make_xml_rpc_api_call.assert_called_with( 'ENDPOINT/SoftLayer_SERVICE', 'METHOD', (), headers={ 'authenticate': { @@ -261,8 +261,8 @@ def test_mask_call_v2_dot(self, make_api_call): 'User-Agent': USER_AGENT, }) - @patch('SoftLayer.API.make_api_call') - def test_mask_call_invalid_mask(self, make_api_call): + @patch('SoftLayer.API.make_xml_rpc_api_call') + def test_mask_call_invalid_mask(self, make_xml_rpc_api_call): try: self.client['SERVICE'].METHOD(mask="mask[something.nested") except SoftLayer.SoftLayerError, e: diff --git a/SoftLayer/tests/API/functional_tests.py b/SoftLayer/tests/API/functional_tests.py index 4947e09aa..245516eb3 100644 --- a/SoftLayer/tests/API/functional_tests.py +++ b/SoftLayer/tests/API/functional_tests.py @@ -11,9 +11,7 @@ try: import unittest2 as unittest except ImportError: - import unittest # NOQA -from mock import patch -from SoftLayer.transport import xmlrpclib_transport + import unittest # NOQA def get_creds(): @@ -38,14 +36,6 @@ def test_failed_auth(self): self.assertRaises(SoftLayer.SoftLayerAPIError, client.getPortalLoginToken) - @patch('SoftLayer.API.make_api_call', xmlrpclib_transport.make_api_call) - def test_with_xmlrpc_transport(self): - client = SoftLayer.Client( - 'SoftLayer_User_Customer', None, 'doesnotexist', 'issurelywrong', - timeout=20) - self.assertRaises(SoftLayer.SoftLayerAPIError, - client.getPortalLoginToken) - def test_404(self): client = SoftLayer.Client( 'SoftLayer_User_Customer', None, 'doesnotexist', 'issurelywrong', @@ -60,35 +50,10 @@ def test_404(self): except: self.fail('No Exception Raised') - @patch('SoftLayer.API.make_api_call', xmlrpclib_transport.make_api_call) - def test_404_with_xmlrpc_transport(self): - client = SoftLayer.Client( - 'SoftLayer_User_Customer', None, 'doesnotexist', 'issurelywrong', - timeout=20, endpoint_url='http://httpbin.org/status/404') - - try: - client.doSomething() - except SoftLayer.SoftLayerAPIError, e: - self.assertEqual(e.faultCode, 404) - self.assertIn('NOT FOUND', e.faultString) - self.assertIn('NOT FOUND', e.reason) - def test_no_hostname(self): try: # This test will fail if 'notvalidsoftlayer.com' becomes a thing - SoftLayer.API.make_api_call( - 'http://notvalidsoftlayer.com', 'getObject') - except SoftLayer.SoftLayerAPIError, e: - self.assertEqual(e.faultCode, 0) - self.assertIn('not known', e.faultString) - self.assertIn('not known', e.reason) - except: - self.fail('No Exception Raised') - - def test_no_hostname_with_xmlrpc_transport(self): - try: - # This test will fail if 'notvalidsoftlayer.com' becomes a thing - xmlrpclib_transport.make_api_call( + SoftLayer.API.make_xml_rpc_api_call( 'http://notvalidsoftlayer.com', 'getObject') except SoftLayer.SoftLayerAPIError, e: self.assertEqual(e.faultCode, 0) diff --git a/SoftLayer/tests/API/metadata_tests.py b/SoftLayer/tests/API/metadata_tests.py index d27622931..5d23e0104 100644 --- a/SoftLayer/tests/API/metadata_tests.py +++ b/SoftLayer/tests/API/metadata_tests.py @@ -10,17 +10,8 @@ try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import patch, MagicMock -import urllib2 -import sys - -if sys.version_info >= (3,): - REQ_PATH = 'urllib.request.Request' - URLOPEN_PATH = 'urllib.request.urlopen' -else: - REQ_PATH = 'urllib2.Request' - URLOPEN_PATH = 'urllib2.urlopen' class MetadataTests(unittest.TestCase): @@ -96,17 +87,26 @@ def setUp(self): 'SoftLayer_Resource_Metadata', 'something.json']) - @patch(REQ_PATH) - @patch(URLOPEN_PATH) - def test_basic(self, urlopen, req): + @patch('SoftLayer.metadata.make_rest_api_call') + def test_basic(self, make_api_call): r = self.metadata.make_request('something.json') - req.assert_called_with(self.url) - self.assertEqual(r, urlopen().read()) - - @patch(REQ_PATH) - @patch(URLOPEN_PATH) - def test_raise_urlerror(self, urlopen, req): - urlopen.side_effect = urllib2.URLError('Error') + make_api_call.assert_called_with( + 'GET', self.url, + timeout=5, + http_headers={'User-Agent': 'SoftLayer Python v2.2.0'}) + self.assertEqual(make_api_call(), r) + + @patch('SoftLayer.metadata.make_rest_api_call') + def test_raise_error(self, make_api_call): + make_api_call.side_effect = SoftLayer.SoftLayerAPIError( + 'faultCode', 'faultString') self.assertRaises( SoftLayer.SoftLayerAPIError, self.metadata.make_request, 'something.json') + + @patch('SoftLayer.metadata.make_rest_api_call') + def test_raise_404_error(self, make_api_call): + make_api_call.side_effect = SoftLayer.SoftLayerAPIError( + 404, 'faultString') + r = self.metadata.make_request('something.json') + self.assertEqual(r, None) diff --git a/SoftLayer/tests/API/transport_tests.py b/SoftLayer/tests/API/transport_tests.py index e69de29bb..729f54d5b 100644 --- a/SoftLayer/tests/API/transport_tests.py +++ b/SoftLayer/tests/API/transport_tests.py @@ -0,0 +1,75 @@ +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 +from requests import HTTPError, RequestException + + +class TestRestAPICall(unittest.TestCase): + + @patch('SoftLayer.transport.requests.request') + def test_json(self, request): + request().content = '{}' + resp = make_rest_api_call('GET', 'http://something.com/path/to/resourse.json') + self.assertEqual(resp, {}) + request.assert_called_with( + 'GET', 'http://something.com/path/to/resourse.json', + headers=None, + timeout=None) + + # Test JSON Error + e = HTTPError('error') + e.response = MagicMock() + e.response.status_code = 404 + e.response.content = '''{ + "error": "description", + "code": "Error Code" + }''' + request().raise_for_status.side_effect = e + + self.assertRaises( + SoftLayerAPIError, + make_rest_api_call, + 'GET', + 'http://something.com/path/to/resourse.json') + + @patch('SoftLayer.transport.requests.request') + def test_text(self, request): + request().text = 'content' + resp = make_rest_api_call('GET', 'http://something.com/path/to/resourse.txt') + self.assertEqual(resp, 'content') + request.assert_called_with( + 'GET', 'http://something.com/path/to/resourse.txt', + headers=None, + timeout=None) + + # Test Text Error + e = HTTPError('error') + e.response = MagicMock() + e.response.status_code = 404 + e.response.content = 'Error Code' + request().raise_for_status.side_effect = e + + self.assertRaises( + SoftLayerAPIError, + make_rest_api_call, + 'GET', + 'http://something.com/path/to/resourse.txt') + + @patch('SoftLayer.transport.requests.request') + def test_unknown_error(self, request): + e = RequestException('error') + e.response = MagicMock() + e.response.status_code = 404 + e.response.content = 'Error Code' + request().raise_for_status.side_effect = e + + self.assertRaises( + TransportError, + make_rest_api_call, + 'GET', + 'http://something.com/path/to/resourse.txt') diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 65471bf5d..29a2b422c 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -12,7 +12,6 @@ import unittest # NOQA from mock import patch - import SoftLayer.CLI as cli if sys.version_info >= (3,): diff --git a/SoftLayer/transport/requests_transport.py b/SoftLayer/transport.py similarity index 54% rename from SoftLayer/transport/requests_transport.py rename to SoftLayer/transport.py index d999d2f83..a10e9a625 100644 --- a/SoftLayer/transport/requests_transport.py +++ b/SoftLayer/transport.py @@ -1,6 +1,6 @@ """ - SoftLayer.transport.requests_transport - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.transport + ~~~~~~~~~~~~~~~~~~~ XML-RPC transport layer that uses the requests library. :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. @@ -12,10 +12,20 @@ ApplicationError, RemoteSystemError, TransportError) import xmlrpclib import requests +import json -def make_api_call(uri, method, args=None, headers=None, - http_headers=None, timeout=None, verbose=False): +def make_xml_rpc_api_call(uri, method, args=None, headers=None, + http_headers=None, timeout=None, verbose=False): + """ Makes a SoftLayer API call against the XML-RPC endpoint + + :param string uri: endpoint URL + :param string method: method to call E.G.: 'getObject' + :param dict headers: XML-RPC headers to use for the request + :param dict http_headers: HTTP headers to use for the request + :param int timeout: number of seconds to use as a timeout + :param bool verbose: verbosity + """ if args is None: args = tuple() try: @@ -24,7 +34,6 @@ def make_api_call(uri, method, args=None, headers=None, payload = xmlrpclib.dumps(tuple(largs), methodname=method, allow_none=True) - response = requests.post(uri, data=payload, headers=http_headers, timeout=timeout) @@ -53,3 +62,29 @@ def make_api_call(uri, method, args=None, headers=None, raise TransportError(e.response.status_code, str(e)) except requests.RequestException, e: raise TransportError(0, str(e)) + + +def make_rest_api_call(method, url, http_headers=None, timeout=None): + """ Makes a SoftLayer API call against the REST endpoint + + :param string method: HTTP method: GET, POST, PUT, DELETE + :param string url: endpoint URL + :param dict http_headers: HTTP headers to use for the request + :param int timeout: number of seconds to use as a timeout + """ + resp = requests.request(method, url, headers=http_headers, timeout=timeout) + try: + resp.raise_for_status() + except requests.HTTPError, 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: + raise TransportError(0, str(e)) + + if url.endswith('.json'): + return json.loads(resp.content) + else: + return resp.text diff --git a/SoftLayer/transport/__init__.py b/SoftLayer/transport/__init__.py deleted file mode 100644 index 4368b3855..000000000 --- a/SoftLayer/transport/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - from SoftLayer.transport.requests_transport import make_api_call -except ImportError: # pragma: no cover - from SoftLayer.transport.xmlrpclib_transport import make_api_call diff --git a/SoftLayer/transport/xmlrpclib_transport.py b/SoftLayer/transport/xmlrpclib_transport.py deleted file mode 100644 index f82616892..000000000 --- a/SoftLayer/transport/xmlrpclib_transport.py +++ /dev/null @@ -1,81 +0,0 @@ -""" - SoftLayer.transport.requests_transport - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - XML-RPC transport layer that uses the built-in xmlrpclib library. This - exists to support Python 2.5. - - :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. - :license: BSD, see LICENSE for more details. -""" -from SoftLayer.exceptions import ( - SoftLayerAPIError, NotWellFormed, UnsupportedEncoding, InvalidCharacter, - SpecViolation, MethodNotFound, InvalidMethodParameters, InternalError, - ApplicationError, RemoteSystemError, TransportError) -from urlparse import urlparse -import xmlrpclib - -import socket - - -def make_api_call(uri, method, args=None, headers=None, http_headers=None, - timeout=None, verbose=False): - if args is None: - args = tuple() - http_protocol = urlparse(uri).scheme - - if http_protocol == "https": - transport = SecureProxyTransport() - else: - transport = ProxyTransport() - - if http_headers: - for name, value in http_headers.items(): - transport.set_raw_header(name, value) - - __prevDefaultTimeout = socket.getdefaulttimeout() - try: - socket.setdefaulttimeout(timeout) - proxy = xmlrpclib.ServerProxy(uri, transport=transport, - verbose=verbose, allow_none=True) - return getattr(proxy, method)({'headers': headers}, *args) - except xmlrpclib.Fault, e: - # These exceptions are formed from the XML-RPC spec - # http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php - error_mapping = { - '-32700': NotWellFormed, - '-32701': UnsupportedEncoding, - '-32702': InvalidCharacter, - '-32600': SpecViolation, - '-32601': MethodNotFound, - '-32602': InvalidMethodParameters, - '-32603': InternalError, - '-32500': ApplicationError, - '-32400': RemoteSystemError, - '-32300': TransportError, - } - raise error_mapping.get(e.faultCode, SoftLayerAPIError)( - e.faultCode, e.faultString) - except xmlrpclib.ProtocolError, e: - raise TransportError(e.errcode, e.errmsg) - except socket.error, e: - raise TransportError(0, str(e)) - finally: - socket.setdefaulttimeout(__prevDefaultTimeout) - - -class ProxyTransport(xmlrpclib.Transport): - __extra_headers = None - - def set_raw_header(self, name, value): - if self.__extra_headers is None: - self.__extra_headers = {} - self.__extra_headers[name] = value - - def send_user_agent(self, connection): - if self.__extra_headers: - for k, v in self.__extra_headers.iteritems(): - connection.putheader(k, v) - - -class SecureProxyTransport(xmlrpclib.SafeTransport, ProxyTransport): - __extra_headers = {} diff --git a/setup.py b/setup.py index 45a5c916c..7ae4e103f 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,29 @@ -from setuptools import setup, find_packages +try: + from setuptools import setup +except ImportError: + from distutils.core import setup # NOQA import sys import os +# Not supported for Python versions < 2.6 +if sys.version_info <= (2, 6): + print("Python 2.6 or greater is required.") + sys.exit(1) + # Python 3 conversion extra = {} if sys.version_info >= (3,): extra['use_2to3'] = True -requires = ['distribute', 'prettytable', 'docopt==0.6.1'] +requires = [ + 'distribute', + 'prettytable', + 'docopt==0.6.1', + 'requests' +] if sys.version_info < (2, 7): requires.append('importlib') -elif sys.version_info >= (2, 6): - requires.append('requests') -if sys.version_info <= (2, 6): - requires.append('simplejson') description = "A library to use SoftLayer's API" @@ -34,7 +43,14 @@ long_description=long_description, author='SoftLayer Technologies, Inc.', author_email='sldn@softlayer.com', - packages=find_packages(), + packages=[ + 'SoftLayer', + 'SoftLayer.CLI', + 'SoftLayer.CLI.modules', + 'SoftLayer.tests', + 'SoftLayer.tests.API', + 'SoftLayer.tests.CLI' + ], license='The BSD License', zip_safe=False, url='http://github.com/softlayer/softlayer-api-python-client', @@ -55,13 +71,11 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries :: Python Modules', - 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], diff --git a/tox.ini b/tox.ini index 0cf1f4a2b..931c60a03 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py25,py26,py27,py32,py33,pypy +envlist = py26,py27,py32,py33,pypy [testenv] commands = nosetests [] From 8901ca5f75c20c21c642a9b1f25b51833366fd0a Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 20 May 2013 09:28:36 -0500 Subject: [PATCH 0031/3227] Fixes the Metadata Manager JSON Decoding Twice --- SoftLayer/metadata.py | 7 +------ SoftLayer/tests/API/metadata_tests.py | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/SoftLayer/metadata.py b/SoftLayer/metadata.py index 3f11e9d27..9306d0eb6 100644 --- a/SoftLayer/metadata.py +++ b/SoftLayer/metadata.py @@ -6,8 +6,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import json - from SoftLayer.transport import make_rest_api_call from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST, USER_AGENT from SoftLayer.exceptions import SoftLayerAPIError, SoftLayerError @@ -93,10 +91,7 @@ def get(self, name, param=None): else: path = "%s%s" % (self.attribs[name]['call'], extension) - data = self.make_request(path) - if data and extension == '.json': - return json.loads(data) - return data + return self.make_request(path) def _get_network(self, kind, router=True, vlans=True, vlan_ids=True): network = {} diff --git a/SoftLayer/tests/API/metadata_tests.py b/SoftLayer/tests/API/metadata_tests.py index 5d23e0104..df80d57b7 100644 --- a/SoftLayer/tests/API/metadata_tests.py +++ b/SoftLayer/tests/API/metadata_tests.py @@ -22,13 +22,13 @@ def setUp(self): self.metadata.make_request = self.make_request def test_no_param(self): - self.make_request.return_value = '"dal01"' + self.make_request.return_value = 'dal01' r = self.metadata.get('datacenter') self.make_request.assert_called_with("Datacenter.json") self.assertEqual('dal01', r) def test_w_param(self): - self.make_request.return_value = '[123]' + self.make_request.return_value = [123] r = self.metadata.get('vlans', '1:2:3:4:5') self.make_request.assert_called_with("Vlans/1:2:3:4:5.json") self.assertEqual([123], r) @@ -53,28 +53,27 @@ def test_not_exists(self): SoftLayer.SoftLayerError, self.metadata.get, 'something') def test_networks_not_exist(self): - self.make_request.return_value = '[]' + self.make_request.return_value = [] r = self.metadata.public_network() self.assertEqual({'mac_addresses': []}, r) def test_networks(self): - resp = '["list", "of", "stuff"]' - resp_list = ['list', 'of', 'stuff'] + resp = ['list', 'of', 'stuff'] self.make_request.return_value = resp r = self.metadata.public_network() self.assertEqual({ - 'vlan_ids': resp_list, - 'router': resp_list, - 'vlans': resp_list, - 'mac_addresses': resp_list + 'vlan_ids': resp, + 'router': resp, + 'vlans': resp, + 'mac_addresses': resp }, r) r = self.metadata.private_network() self.assertEqual({ - 'vlan_ids': resp_list, - 'router': resp_list, - 'vlans': resp_list, - 'mac_addresses': resp_list + 'vlan_ids': resp, + 'router': resp, + 'vlans': resp, + 'mac_addresses': resp }, r) From 1294a82f927c677fa6e6b92f4f203aa118a61d28 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 20 May 2013 10:01:29 -0500 Subject: [PATCH 0032/3227] Pins prettytable to greater than version 0.7.0 I had issues testing this because the CCI I was using already had prettytable version 0.5. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7ae4e103f..c0391e99a 100644 --- a/setup.py +++ b/setup.py @@ -17,8 +17,8 @@ requires = [ 'distribute', - 'prettytable', - 'docopt==0.6.1', + 'prettytable >= 0.7.0', + 'docopt == 0.6.1', 'requests' ] From d645620421ecacc0cae02df73a4de0cd8234eda2 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 21 May 2013 11:02:15 -0400 Subject: [PATCH 0033/3227] Removes python 2.5 from README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 496da2123..6e53d3c5c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ library. System Requirements ------------------- -* This library has been tested on Python 2.5, 2.6, 2.7, 3.2 and 3.3. +* This library has been tested on Python 2.6, 2.7, 3.2 and 3.3. * A valid SoftLayer API username and key are required to call SoftLayer's API * A connection to SoftLayer's private network is required to connect to SoftLayer’s private network API endpoints. From a59b8f6e4f6e8fa4bb1ed95ac11bfbfbdafc07d4 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 24 May 2013 09:55:07 -0500 Subject: [PATCH 0034/3227] Refactor Managers * Moves managers to SoftLayer.managers * Moves manager tests to SoftLayer.tests.managers * Adds missing manager imports so that all managers are exposed as SoftLayer.ManagerClass * Adds missing hardware manager to the docs * Adds Firewall Manager Test * Renames capital manager modules to lowercase * Moves manager exceptions to SoftLayer.exceptions * Adds a couple missing module doc blocks --- SoftLayer/CLI/modules/cci.py | 4 +- SoftLayer/CLI/modules/dns.py | 2 +- SoftLayer/CLI/modules/firewall.py | 2 +- SoftLayer/CLI/modules/hardware.py | 2 +- SoftLayer/CLI/modules/metadata.py | 2 +- SoftLayer/CLI/modules/ssl.py | 2 +- SoftLayer/__init__.py | 8 +-- SoftLayer/exceptions.py | 4 ++ SoftLayer/managers/__init__.py | 19 +++++ SoftLayer/{CCI.py => managers/cci.py} | 16 +---- SoftLayer/{DNS.py => managers/dns.py} | 9 +-- SoftLayer/{ => managers}/firewall.py | 29 ++++---- SoftLayer/{ => managers}/hardware.py | 9 +++ SoftLayer/{ => managers}/metadata.py | 1 - SoftLayer/{SSL.py => managers/ssl.py} | 1 - .../{API/client_tests.py => api_tests.py} | 17 +++-- SoftLayer/tests/{API => }/functional_tests.py | 4 +- SoftLayer/tests/{API => managers}/__init__.py | 0 .../tests/{API => managers}/cci_tests.py | 71 +++++++++---------- .../tests/{API => managers}/dns_tests.py | 48 +++++++------ SoftLayer/tests/managers/firewall_tests.py | 33 +++++++++ .../tests/{API => managers}/hardware_tests.py | 11 ++- .../tests/{API => managers}/metadata_tests.py | 31 ++++---- .../tests/{API => managers}/ssl_tests.py | 35 ++++----- SoftLayer/tests/{API => }/transport_tests.py | 7 ++ docs/api/managers.rst | 3 +- docs/api/managers/cci.rst | 2 +- docs/api/managers/dns.rst | 2 +- docs/api/managers/firewall.rst | 2 +- docs/api/managers/hardware.rst | 6 ++ docs/api/managers/metadata.rst | 4 +- docs/api/managers/ssl.rst | 2 +- docs/install.rst | 2 +- setup.py | 5 +- 34 files changed, 225 insertions(+), 170 deletions(-) create mode 100644 SoftLayer/managers/__init__.py rename SoftLayer/{CCI.py => managers/cci.py} (95%) rename SoftLayer/{DNS.py => managers/dns.py} (96%) rename SoftLayer/{ => managers}/firewall.py (55%) rename SoftLayer/{ => managers}/hardware.py (95%) rename SoftLayer/{ => managers}/metadata.py (99%) rename SoftLayer/{SSL.py => managers/ssl.py} (98%) rename SoftLayer/tests/{API/client_tests.py => api_tests.py} (98%) rename SoftLayer/tests/{API => }/functional_tests.py (97%) rename SoftLayer/tests/{API => managers}/__init__.py (100%) rename SoftLayer/tests/{API => managers}/cci_tests.py (87%) rename SoftLayer/tests/{API => managers}/dns_tests.py (65%) create mode 100644 SoftLayer/tests/managers/firewall_tests.py rename SoftLayer/tests/{API => managers}/hardware_tests.py (92%) rename SoftLayer/tests/{API => managers}/metadata_tests.py (77%) rename SoftLayer/tests/{API => managers}/ssl_tests.py (58%) rename SoftLayer/tests/{API => }/transport_tests.py (92%) create mode 100644 docs/api/managers/hardware.rst diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index babd02e44..b6503f4f4 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -25,7 +25,7 @@ from os import linesep import os.path -from SoftLayer.CCI import CCIManager +from SoftLayer import CCIManager from SoftLayer.CLI import ( CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, FormattedItem) @@ -693,7 +693,7 @@ def execute(cls, client, args): @staticmethod def dns_sync(client, args): - from SoftLayer.DNS import DNSManager, DNSZoneNotFound + from SoftLayer import DNSManager, DNSZoneNotFound dns = DNSManager(client) cci = CCIManager(client) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 9f208dff1..33b703e77 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -17,7 +17,7 @@ # :license: BSD, see LICENSE for more details. from SoftLayer.CLI import CLIRunnable, no_going_back, Table, CLIAbort -from SoftLayer.DNS import DNSManager, DNSZoneNotFound +from SoftLayer import DNSManager, DNSZoneNotFound class DumpZone(CLIRunnable): diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py index 37982b0f6..ef2b05721 100755 --- a/SoftLayer/CLI/modules/firewall.py +++ b/SoftLayer/CLI/modules/firewall.py @@ -11,7 +11,7 @@ from SoftLayer.CLI import CLIRunnable, Table, listing from SoftLayer.CLI.helpers import blank -from SoftLayer.firewall import FirewallManager +from SoftLayer import FirewallManager class FWList(CLIRunnable): diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index efadbbabc..1d040a3a3 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -15,7 +15,7 @@ from SoftLayer.CLI.helpers import ( CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, gb, no_going_back) -from SoftLayer.hardware import HardwareManager +from SoftLayer import HardwareManager def resolve_id(manager, identifier): diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index 9cc1a797b..f70757687 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -23,7 +23,7 @@ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. -from SoftLayer.metadata import MetadataManager +from SoftLayer import MetadataManager from SoftLayer.CLI import CLIRunnable, Table, listing, CLIAbort diff --git a/SoftLayer/CLI/modules/ssl.py b/SoftLayer/CLI/modules/ssl.py index 393655281..9013172b7 100755 --- a/SoftLayer/CLI/modules/ssl.py +++ b/SoftLayer/CLI/modules/ssl.py @@ -16,7 +16,7 @@ from SoftLayer.CLI.helpers import CLIRunnable, no_going_back, Table, CLIAbort from SoftLayer.CLI.helpers import blank -from SoftLayer.SSL import SSLManager +from SoftLayer import SSLManager class ListCerts(CLIRunnable): diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 6fb27cce5..3ff4ea1de 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -18,10 +18,7 @@ from API import ( Client, BasicAuthentication, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) -from DNS import DNSManager -from CCI import CCIManager -from metadata import MetadataManager -from hardware import HardwareManager +from managers import * # NOQA from SoftLayer.exceptions import * # NOQA __title__ = 'SoftLayer' @@ -30,5 +27,4 @@ __license__ = 'The BSD License' __copyright__ = 'Copyright 2013 SoftLayer Technologies, Inc.' __all__ = ['Client', 'BasicAuthentication', 'SoftLayerError', - 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', - 'DNSManager', 'CCIManager', 'MetadataManager', 'HardwareManager'] + 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] diff --git a/SoftLayer/exceptions.py b/SoftLayer/exceptions.py index 72d82f23c..6488ef0d6 100644 --- a/SoftLayer/exceptions.py +++ b/SoftLayer/exceptions.py @@ -78,3 +78,7 @@ class InvalidMethodParameters(ServerError): class InternalError(ServerError): pass + + +class DNSZoneNotFound(SoftLayerError): + pass diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py new file mode 100644 index 000000000..bb821caa6 --- /dev/null +++ b/SoftLayer/managers/__init__.py @@ -0,0 +1,19 @@ +""" + SoftLayer.managers + ~~~~~~~~~~~~~~~~~~ + 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. + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer.managers.cci import CCIManager +from SoftLayer.managers.dns import DNSManager +from SoftLayer.managers.firewall import FirewallManager +from SoftLayer.managers.hardware import HardwareManager +from SoftLayer.managers.metadata import MetadataManager +from SoftLayer.managers.ssl import SSLManager + +__all__ = ['CCIManager', 'DNSManager', 'FirewallManager', 'HardwareManager', + 'MetadataManager', 'SSLManager'] diff --git a/SoftLayer/CCI.py b/SoftLayer/managers/cci.py similarity index 95% rename from SoftLayer/CCI.py rename to SoftLayer/managers/cci.py index 068adf2eb..62ec47a59 100644 --- a/SoftLayer/CCI.py +++ b/SoftLayer/managers/cci.py @@ -10,20 +10,9 @@ from time import sleep from itertools import repeat -from SoftLayer.exceptions import SoftLayerError from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin -class CCICreateMissingRequired(SoftLayerError): - def __init__(self): - self.message = "cpu, memory, hostname, and domain are required" - - -class CCICreateMutuallyExclusive(SoftLayerError): - def __init__(self, *args): - self.message = "Can only specify one of:", ','.join(args) - - class CCIManager(IdentifierMixin, object): """ Manage CCIs """ def __init__(self, client): @@ -194,11 +183,12 @@ def _generate_create_dict( ] if not all(required): - raise CCICreateMissingRequired() + raise ValueError("cpu, memory, hostname, and domain are required") for me in mutually_exclusive: if all(me.values()): - raise CCICreateMutuallyExclusive(*me.keys()) + raise ValueError( + 'Can only specify one of: %s' % (','.join(me.keys()))) data = { "startCpus": int(cpus), diff --git a/SoftLayer/DNS.py b/SoftLayer/managers/dns.py similarity index 96% rename from SoftLayer/DNS.py rename to SoftLayer/managers/dns.py index 8bb1fcb6e..eb70fbad1 100644 --- a/SoftLayer/DNS.py +++ b/SoftLayer/managers/dns.py @@ -7,14 +7,7 @@ :license: BSD, see LICENSE for more details. """ from time import strftime -from SoftLayer.exceptions import SoftLayerError - - -__all__ = ["DNSZoneNotFound", "DNSManager"] - - -class DNSZoneNotFound(SoftLayerError): - pass +from SoftLayer.exceptions import DNSZoneNotFound class DNSManager(object): diff --git a/SoftLayer/firewall.py b/SoftLayer/managers/firewall.py similarity index 55% rename from SoftLayer/firewall.py rename to SoftLayer/managers/firewall.py index cd59b17c2..626605dd0 100644 --- a/SoftLayer/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -6,7 +6,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -__all__ = ['FirewallManager'] def has_firewall(vlan): @@ -29,16 +28,18 @@ def __init__(self, client): self.client = client def get_firewalls(self): - results = filter(has_firewall, self.client['Account'].getObject( - mask={'networkVlans': { - 'firewallNetworkComponents': None, - 'networkVlanFirewall': None, - 'dedicatedFirewallFlag': None, - 'firewallGuestNetworkComponents': None, - 'firewallInterfaces': {}, - 'firewallRules': None, - 'highAvailabilityFirewallFlag': None, - #'primarySubnet': None, - }})['networkVlans']) - - return results + results = self.client['Account'].getObject( + mask={ + 'networkVlans': { + 'firewallNetworkComponents': None, + 'networkVlanFirewall': None, + 'dedicatedFirewallFlag': None, + 'firewallGuestNetworkComponents': None, + 'firewallInterfaces': {}, + 'firewallRules': None, + 'highAvailabilityFirewallFlag': None, + #'primarySubnet': None, + } + })['networkVlans'] + + return filter(has_firewall, results) diff --git a/SoftLayer/hardware.py b/SoftLayer/managers/hardware.py similarity index 95% rename from SoftLayer/hardware.py rename to SoftLayer/managers/hardware.py index acdb8b58e..018aeb266 100644 --- a/SoftLayer/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -1,3 +1,12 @@ +""" + SoftLayer.hardware + ~~~~~~~~~~~~~~~~~~ + Hardware Manager/helpers + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" + import socket from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin diff --git a/SoftLayer/metadata.py b/SoftLayer/managers/metadata.py similarity index 99% rename from SoftLayer/metadata.py rename to SoftLayer/managers/metadata.py index 9306d0eb6..e4b8435be 100644 --- a/SoftLayer/metadata.py +++ b/SoftLayer/managers/metadata.py @@ -10,7 +10,6 @@ from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST, USER_AGENT from SoftLayer.exceptions import SoftLayerAPIError, SoftLayerError -__all__ = ["MetadataManager"] METADATA_MAPPING = { 'backend_mac': {'call': 'BackendMacAddresses'}, diff --git a/SoftLayer/SSL.py b/SoftLayer/managers/ssl.py similarity index 98% rename from SoftLayer/SSL.py rename to SoftLayer/managers/ssl.py index 893cef292..2d4f4b9d3 100644 --- a/SoftLayer/SSL.py +++ b/SoftLayer/managers/ssl.py @@ -6,7 +6,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -__all__ = ["SSLManager"] class SSLManager(object): diff --git a/SoftLayer/tests/API/client_tests.py b/SoftLayer/tests/api_tests.py similarity index 98% rename from SoftLayer/tests/API/client_tests.py rename to SoftLayer/tests/api_tests.py index 5c37f2dd6..4a84f555b 100644 --- a/SoftLayer/tests/API/client_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -1,6 +1,6 @@ """ - SoftLayer.tests.API.client_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.api_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. @@ -112,11 +112,7 @@ def test_service_repr(self): self.assertIn("Service", repr(client['SERVICE'])) -class APICalls(unittest.TestCase): - def setUp(self): - self.client = SoftLayer.Client( - username='doesnotexist', api_key='issurelywrong', - endpoint_url="ENDPOINT") +class OldAPIClient(unittest.TestCase): @patch('SoftLayer.API.make_xml_rpc_api_call') def test_old_api(self, make_xml_rpc_api_call): @@ -181,6 +177,13 @@ def test_old_api_no_service(self): api_key='issurelywrong') self.assertRaises(SoftLayer.SoftLayerError, client.METHOD) + +class APIClient(unittest.TestCase): + def setUp(self): + self.client = SoftLayer.Client( + username='doesnotexist', api_key='issurelywrong', + endpoint_url="ENDPOINT") + @patch('SoftLayer.API.make_xml_rpc_api_call') def test_simple_call(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD() diff --git a/SoftLayer/tests/API/functional_tests.py b/SoftLayer/tests/functional_tests.py similarity index 97% rename from SoftLayer/tests/API/functional_tests.py rename to SoftLayer/tests/functional_tests.py index 245516eb3..1bc07d381 100644 --- a/SoftLayer/tests/API/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -1,6 +1,6 @@ """ - SoftLayer.tests.API.functional_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.functional_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. diff --git a/SoftLayer/tests/API/__init__.py b/SoftLayer/tests/managers/__init__.py similarity index 100% rename from SoftLayer/tests/API/__init__.py rename to SoftLayer/tests/managers/__init__.py diff --git a/SoftLayer/tests/API/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py similarity index 87% rename from SoftLayer/tests/API/cci_tests.py rename to SoftLayer/tests/managers/cci_tests.py index 2acd7f676..151c5364b 100644 --- a/SoftLayer/tests/API/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -1,29 +1,28 @@ """ - SoftLayer.tests.API.cci_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.cci_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer -import SoftLayer.CCI +from SoftLayer import CCIManager try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import MagicMock, ANY, call, patch -class CCITests_unittests(unittest.TestCase): +class CCITests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.cci = SoftLayer.CCIManager(self.client) + self.cci = CCIManager(self.client) def test_list_instances(self): mcall = call(mask=ANY, filter={}) - service = self.client.__getitem__() + service = self.client['Account'] self.cci.list_instances(hourly=True, monthly=True) service.getVirtualGuests.assert_has_calls(mcall) @@ -53,7 +52,7 @@ def test_list_instances_with_filters(self): private_ip='4.3.2.1', ) - service = self.client.__getitem__() + service = self.client['Account'] service.getVirtualGuests.assert_has_calls(call( filter={ 'virtualGuests': { @@ -77,13 +76,13 @@ def test_list_instances_with_filters(self): )) def test_resolve_ids_ip(self): - self.client.__getitem__().getVirtualGuests.return_value = \ - [{'id': '1234'}] + self.client['Account'].getVirtualGuests.return_value = [{'id': '1234'}] _id = self.cci._get_ids_from_ip('1.2.3.4') self.assertEqual(_id, ['1234']) - self.client.__getitem__().getVirtualGuests.side_effect = \ - [[], [{'id': '4321'}]] + self.client['Account'].getVirtualGuests.side_effect = [ + [], [{'id': '4321'}] + ] _id = self.cci._get_ids_from_ip('4.3.2.1') self.assertEqual(_id, ['4321']) @@ -91,51 +90,52 @@ def test_resolve_ids_ip(self): self.assertEqual(_id, []) def test_resolve_ids_hostname(self): - self.client.__getitem__().getVirtualGuests.return_value = \ + self.client['Account'].getVirtualGuests.return_value = \ [{'id': '1234'}] _id = self.cci._get_ids_from_hostname('hostname') self.assertEqual(_id, ['1234']) def test_get_instance(self): - self.client.__getitem__().getObject.return_value = { + self.client['Virtual_Guest'].getObject.return_value = { 'hourlyVirtualGuests': "this is unique"} self.cci.get_instance(1) - self.client.__getitem__().getObject.assert_called_once_with( + self.client['Virtual_Guest'].getObject.assert_called_once_with( id=1, mask=ANY) def test_get_create_options(self): self.cci.get_create_options() - f = self.client.__getitem__().getCreateObjectOptions + f = self.client['Virtual_Guest'].getCreateObjectOptions f.assert_called_once_with() def test_cancel_instance(self): self.cci.cancel_instance(id=1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + self.client['Virtual_Guest'].deleteObject.assert_called_once_with(id=1) def test_reload_instance(self): self.cci.reload_instance(id=1) - f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration + service = self.client['Virtual_Guest'] + f = service.reloadCurrentOperatingSystemConfiguration f.assert_called_once_with(id=1) - @patch('SoftLayer.CCI.CCIManager._generate_create_dict') + @patch('SoftLayer.managers.cci.CCIManager._generate_create_dict') def test_create_verify(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} self.cci.verify_create_instance(test=1, verify=1) create_dict.assert_called_once_with(test=1, verify=1) - f = self.client.__getitem__().generateOrderTemplate + f = self.client['Virtual_Guest'].generateOrderTemplate f.assert_called_once_with({'test': 1, 'verify': 1}) - @patch('SoftLayer.CCI.CCIManager._generate_create_dict') + @patch('SoftLayer.managers.cci.CCIManager._generate_create_dict') def test_create_instance(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} self.cci.create_instance(test=1, verify=1) create_dict.assert_called_once_with(test=1, verify=1) - self.client.__getitem__().createObject.assert_called_once_with( + self.client['Virtual_Guest'].createObject.assert_called_once_with( {'test': 1, 'verify': 1}) def test_generate_os_and_image(self): self.assertRaises( - SoftLayer.CCI.CCICreateMutuallyExclusive, + ValueError, self.cci._generate_create_dict, cpus=1, memory=1, @@ -146,15 +146,8 @@ def test_generate_os_and_image(self): ) def test_generate_missing(self): - self.assertRaises( - SoftLayer.CCI.CCICreateMissingRequired, - self.cci._generate_create_dict, - ) - self.assertRaises( - SoftLayer.CCI.CCICreateMissingRequired, - self.cci._generate_create_dict, - cpus=1 - ) + self.assertRaises(ValueError, self.cci._generate_create_dict) + self.assertRaises(ValueError, self.cci._generate_create_dict, cpus=1) def test_generate_basic(self): data = self.cci._generate_create_dict( @@ -381,7 +374,7 @@ def test_generate_single_disk(self): assert_data = { 'blockDevices': [ - {"device": "0", "diskImage":{"capacity": 50}}] + {"device": "0", "diskImage": {"capacity": 50}}] } self.assertTrue(data.get('blockDevices')) @@ -399,17 +392,17 @@ def test_generate_multi_disk(self): assert_data = { 'blockDevices': [ - {"device": "0", "diskImage":{"capacity": 50}}, - {"device": "2", "diskImage":{"capacity": 70}}, - {"device": "3", "diskImage":{"capacity": 100}}] + {"device": "0", "diskImage": {"capacity": 50}}, + {"device": "2", "diskImage": {"capacity": 70}}, + {"device": "3", "diskImage": {"capacity": 100}}] } self.assertTrue(data.get('blockDevices')) self.assertEqual(data['blockDevices'], assert_data['blockDevices']) - @patch('SoftLayer.CCI.sleep') + @patch('SoftLayer.managers.cci.sleep') def test_wait(self, _sleep): - guestObject = self.client.__getitem__().getObject + guestObject = self.client['Virtual_Guest'].getObject # test 4 iterations with positive match guestObject.side_effect = [ diff --git a/SoftLayer/tests/API/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py similarity index 65% rename from SoftLayer/tests/API/dns_tests.py rename to SoftLayer/tests/managers/dns_tests.py index 2de214d18..c80c19161 100644 --- a/SoftLayer/tests/API/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -1,16 +1,16 @@ """ - SoftLayer.tests.API.dns_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.dns_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer +from SoftLayer import DNSManager, DNSZoneNotFound try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import MagicMock, ANY @@ -18,7 +18,7 @@ class DNSTests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.dns_client = SoftLayer.DNSManager(self.client) + self.dns_client = DNSManager(self.client) def test_init_exercise(self): self.assertTrue(hasattr(self.dns_client, 'service')) @@ -26,7 +26,7 @@ def test_init_exercise(self): def test_list_zones(self): zone_list = ['test'] - self.client.__getitem__().getDomains.return_value = zone_list + self.client['Account'].getDomains.return_value = zone_list zones = self.dns_client.list_zones() self.assertEqual(zones, zone_list) @@ -37,42 +37,42 @@ def test_get_zone(self): ] # match - self.client.__getitem__().getByDomainName.return_value = \ - zone_list + self.client['Dns_Domain'].getByDomainName.return_value = zone_list res = self.dns_client.get_zone('example.com') self.assertEqual(res, zone_list[1]) # no match - from SoftLayer.DNS import DNSZoneNotFound self.assertRaises( DNSZoneNotFound, self.dns_client.get_zone, 'shouldnt-match.com') def test_create_zone(self): - self.client.__getitem__().createObject.return_value = \ - {'name': 'example.com'} + call = self.client['Dns_Domain'].createObject + call.return_value = {'name': 'example.com'} res = self.dns_client.create_zone('example.com') - self.client.__getitem__().createObject.assert_called_once_with( - {'name': 'example.com', "resourceRecords": {}, "serial": ANY}) + call.assert_called_once_with({ + 'name': 'example.com', "resourceRecords": {}, "serial": ANY + }) self.assertEqual(res, {'name': 'example.com'}) def test_delete_zone(self): self.dns_client.delete_zone(1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + self.client['Dns_Domain'].deleteObject.assert_called_once_with(id=1) def test_edit_zone(self): self.dns_client.edit_zone('example.com') - self.client.__getitem__().editObject.assert_called_once_with( + self.client['Dns_Domain'].editObject.assert_called_once_with( 'example.com') def test_create_record(self): self.dns_client.create_record(1, 'test', 'TXT', 'testing', ttl=1200) - self.client.__getitem__().createObject.assert_called_once_with( + f = self.client['Dns_Domain_ResourceRecord'].createObject + f.assert_called_once_with( { 'domainId': 1, 'ttl': 1200, @@ -83,10 +83,12 @@ def test_create_record(self): def test_delete_record(self): self.dns_client.delete_record(1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + f = self.client['Dns_Domain_ResourceRecord'].deleteObject + f.assert_called_once_with(id=1) def test_search_record(self): - self.client.__getitem__().getByDomainName.return_value = [{ + + self.client['Dns_Domain'].getByDomainName.return_value = [{ 'name': 'example.com', 'resourceRecords': [ {'host': 'TEST1'}, @@ -100,14 +102,14 @@ def test_search_record(self): def test_edit_record(self): self.dns_client.edit_record({'id': 1, 'name': 'test'}) - self.client.__getitem__().editObject.assert_called_once_with( + f = self.client['Dns_Domain_ResourceRecord'].editObject + f.assert_called_once_with( {'id': 1, 'name': 'test'}, id=1 ) def test_dump_zone(self): - self.client.__getitem__().getZoneFileContents.return_value = ( - 'lots of text') + f = self.client['Dns_Domain'].getZoneFileContents + f.return_value = 'lots of text' self.dns_client.dump_zone(1) - self.client.__getitem__().getZoneFileContents.assert_called_once_with( - id=1) + f.assert_called_once_with(id=1) diff --git a/SoftLayer/tests/managers/firewall_tests.py b/SoftLayer/tests/managers/firewall_tests.py new file mode 100644 index 000000000..e5a1c32a7 --- /dev/null +++ b/SoftLayer/tests/managers/firewall_tests.py @@ -0,0 +1,33 @@ +""" + SoftLayer.tests.managers.firewall_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer import FirewallManager + +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +from mock import MagicMock, ANY + + +class FirewallTests(unittest.TestCase): + + def setUp(self): + self.client = MagicMock() + self.firewall = FirewallManager(self.client) + + def test_get_firewalls(self): + vlan = { + 'dedicatedFirewallFlag': True, + } + call = self.client['Account'].getObject + call.return_value = {'networkVlans': [vlan]} + + firewalls = self.firewall.get_firewalls() + + self.assertEquals([vlan], firewalls) + call.assert_called_once_with(mask=ANY) diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py similarity index 92% rename from SoftLayer/tests/API/hardware_tests.py rename to SoftLayer/tests/managers/hardware_tests.py index e415ec218..8181b30f4 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -1,12 +1,11 @@ """ - SoftLayer.tests.API.hardware_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.hardware_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer -import SoftLayer.hardware +from SoftLayer import HardwareManager try: import unittest2 as unittest @@ -15,11 +14,11 @@ from mock import MagicMock, ANY, call -class HardwareTests_unittests(unittest.TestCase): +class HardwareTests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.hardware = SoftLayer.HardwareManager(self.client) + self.hardware = HardwareManager(self.client) def test_list_hardware(self): mcall = call(mask=ANY, filter={}) diff --git a/SoftLayer/tests/API/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py similarity index 77% rename from SoftLayer/tests/API/metadata_tests.py rename to SoftLayer/tests/managers/metadata_tests.py index df80d57b7..15ce5f6bf 100644 --- a/SoftLayer/tests/API/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -1,11 +1,12 @@ """ - SoftLayer.tests.API.metadata_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.metadata_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer +from SoftLayer import MetadataManager, SoftLayerError, SoftLayerAPIError +from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST try: import unittest2 as unittest @@ -17,7 +18,7 @@ class MetadataTests(unittest.TestCase): def setUp(self): - self.metadata = SoftLayer.MetadataManager() + self.metadata = MetadataManager() self.make_request = MagicMock() self.metadata.make_request = self.make_request @@ -46,11 +47,10 @@ def test_return_none(self): self.assertEqual(None, r) def test_w_param_error(self): - self.assertRaises(SoftLayer.SoftLayerError, self.metadata.get, 'vlans') + self.assertRaises(SoftLayerError, self.metadata.get, 'vlans') def test_not_exists(self): - self.assertRaises( - SoftLayer.SoftLayerError, self.metadata.get, 'something') + self.assertRaises(SoftLayerError, self.metadata.get, 'something') def test_networks_not_exist(self): self.make_request.return_value = [] @@ -80,13 +80,13 @@ def test_networks(self): class MetadataTestsMakeRequest(unittest.TestCase): def setUp(self): - self.metadata = SoftLayer.MetadataManager() + self.metadata = MetadataManager() self.url = '/'.join([ - SoftLayer.consts.API_PRIVATE_ENDPOINT_REST.rstrip('/'), + API_PRIVATE_ENDPOINT_REST.rstrip('/'), 'SoftLayer_Resource_Metadata', 'something.json']) - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_basic(self, make_api_call): r = self.metadata.make_request('something.json') make_api_call.assert_called_with( @@ -95,17 +95,16 @@ def test_basic(self, make_api_call): http_headers={'User-Agent': 'SoftLayer Python v2.2.0'}) self.assertEqual(make_api_call(), r) - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_raise_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError( + make_api_call.side_effect = SoftLayerAPIError( 'faultCode', 'faultString') self.assertRaises( - SoftLayer.SoftLayerAPIError, + SoftLayerAPIError, self.metadata.make_request, 'something.json') - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_raise_404_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError( - 404, 'faultString') + make_api_call.side_effect = SoftLayerAPIError(404, 'faultString') r = self.metadata.make_request('something.json') self.assertEqual(r, None) diff --git a/SoftLayer/tests/API/ssl_tests.py b/SoftLayer/tests/managers/ssl_tests.py similarity index 58% rename from SoftLayer/tests/API/ssl_tests.py rename to SoftLayer/tests/managers/ssl_tests.py index 5f887be4a..b8ddd2124 100644 --- a/SoftLayer/tests/API/ssl_tests.py +++ b/SoftLayer/tests/managers/ssl_tests.py @@ -1,11 +1,11 @@ """ - SoftLayer.tests.API.ssl_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.ssl_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -from SoftLayer.SSL import SSLManager +from SoftLayer import SSLManager try: import unittest2 as unittest @@ -14,7 +14,7 @@ from mock import MagicMock, ANY -class SSLTests_unittests(unittest.TestCase): +class SSLTests(unittest.TestCase): def setUp(self): self.client = MagicMock() @@ -23,16 +23,16 @@ def setUp(self): def test_list_certs(self): self.ssl.list_certs('valid') - self.client.__getitem__() \ - .getValidSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getValidSecurityCertificates + f.assert_called_once_with(mask=ANY) self.ssl.list_certs('expired') - self.client.__getitem__() \ - .getExpiredSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getExpiredSecurityCertificates + f.assert_called_once_with(mask=ANY) self.ssl.list_certs('all') - self.client.__getitem__() \ - .getSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getSecurityCertificates + f.assert_called_once_with(mask=ANY) def test_add_certificate(self): test_cert = { @@ -42,13 +42,13 @@ def test_add_certificate(self): self.ssl.add_certificate(test_cert) - self.client.__getitem__().createObject.assert_called_once_with( - test_cert) + f = self.client['Security_Certificate'].createObject + f.assert_called_once_with(test_cert) def test_remove_certificate(self): self.ssl.remove_certificate(self.test_id) - self.client.__getitem__() \ - .deleteObject.assert_called_once_with(id=self.test_id) + f = self.client['Security_Certificate'].deleteObject + f.assert_called_once_with(id=self.test_id) def test_edit_certificate(self): test_cert = { @@ -58,7 +58,8 @@ def test_edit_certificate(self): } self.ssl.edit_certificate(test_cert) - self.client.__getitem__().editObject.assert_called_once_with( + f = self.client['Security_Certificate'].editObject + f.assert_called_once_with( { 'id': self.test_id, 'certificate': 'cert', @@ -68,5 +69,5 @@ def test_edit_certificate(self): def test_get_certificate(self): self.ssl.get_certificate(self.test_id) - self.client.__getitem__().getObject.assert_called_once_with( - id=self.test_id) + f = self.client['Security_Certificate'].getObject + f.assert_called_once_with(id=self.test_id) diff --git a/SoftLayer/tests/API/transport_tests.py b/SoftLayer/tests/transport_tests.py similarity index 92% rename from SoftLayer/tests/API/transport_tests.py rename to SoftLayer/tests/transport_tests.py index 729f54d5b..f9ad470bd 100644 --- a/SoftLayer/tests/API/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -1,3 +1,10 @@ +""" + SoftLayer.tests.transport_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" try: import unittest2 as unittest except ImportError: diff --git a/docs/api/managers.rst b/docs/api/managers.rst index 1f866357a..c2bf1fe83 100644 --- a/docs/api/managers.rst +++ b/docs/api/managers.rst @@ -4,7 +4,8 @@ Managers -------- :: - >>> from SoftLayer.CCI import CCIManager + >>> from SoftLayer import CCIManager, Client + >>> client = Client(...) >>> cci = CCIManager(client) >>> cci.list_instances() [...] diff --git a/docs/api/managers/cci.rst b/docs/api/managers/cci.rst index 5af2a9a1d..d845c44cc 100644 --- a/docs/api/managers/cci.rst +++ b/docs/api/managers/cci.rst @@ -1,6 +1,6 @@ .. _cci: -.. automodule:: SoftLayer.CCI +.. automodule:: SoftLayer.managers.cci :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/dns.rst b/docs/api/managers/dns.rst index 047a1ea49..eef936177 100644 --- a/docs/api/managers/dns.rst +++ b/docs/api/managers/dns.rst @@ -1,6 +1,6 @@ .. _dns: -.. automodule:: SoftLayer.DNS +.. automodule:: SoftLayer.managers.dns :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/firewall.rst b/docs/api/managers/firewall.rst index ec4eda1a2..8bb5cd5b1 100644 --- a/docs/api/managers/firewall.rst +++ b/docs/api/managers/firewall.rst @@ -1,6 +1,6 @@ .. _firewall: -.. automodule:: SoftLayer.firewall +.. automodule:: SoftLayer.managers.firewall :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/hardware.rst b/docs/api/managers/hardware.rst new file mode 100644 index 000000000..6be2bbba8 --- /dev/null +++ b/docs/api/managers/hardware.rst @@ -0,0 +1,6 @@ +.. _hardware: + +.. automodule:: SoftLayer.managers.hardware + :members: + :inherited-members: + :undoc-members: diff --git a/docs/api/managers/metadata.rst b/docs/api/managers/metadata.rst index e5cb87b2d..e37ec86ca 100644 --- a/docs/api/managers/metadata.rst +++ b/docs/api/managers/metadata.rst @@ -1,8 +1,8 @@ .. _metadata: -.. automodule:: SoftLayer.metadata +.. automodule:: SoftLayer.managers.metadata :members: :inherited-members: :undoc-members: -.. autoattribute:: SoftLayer.metadata.METADATA_ATTRIBUTES \ No newline at end of file +.. autoattribute:: SoftLayer.managers.metadata.METADATA_ATTRIBUTES \ No newline at end of file diff --git a/docs/api/managers/ssl.rst b/docs/api/managers/ssl.rst index 17c9e053e..b1fb6dd1f 100644 --- a/docs/api/managers/ssl.rst +++ b/docs/api/managers/ssl.rst @@ -1,6 +1,6 @@ .. _ssl: -.. automodule:: SoftLayer.SSL +.. automodule:: SoftLayer.managers.ssl :members: :inherited-members: :undoc-members: diff --git a/docs/install.rst b/docs/install.rst index f9513a68b..cc6cd6d3a 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -25,7 +25,7 @@ The project is developed on GitHub, at `github.com/softlayer/softlayer-api-pytho You can clone the public repository:: - git clone git://github.com/softlayer/softlayer-api-python-client.git + $ git clone git://github.com/softlayer/softlayer-api-python-client.git Or, Download the `tarball `_:: diff --git a/setup.py b/setup.py index c0391e99a..e9992e716 100644 --- a/setup.py +++ b/setup.py @@ -47,9 +47,10 @@ 'SoftLayer', 'SoftLayer.CLI', 'SoftLayer.CLI.modules', + 'SoftLayer.managers', 'SoftLayer.tests', - 'SoftLayer.tests.API', - 'SoftLayer.tests.CLI' + 'SoftLayer.tests.CLI', + 'SoftLayer.tests.managers', ], license='The BSD License', zip_safe=False, From 28a0605dcd7306482fc8cda6425c9a618b6e0cbc Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 28 May 2013 11:08:35 -0500 Subject: [PATCH 0035/3227] First Attempt at Message Queue Module --- SoftLayer/CLI/modules/messaging.py | 302 +++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 SoftLayer/CLI/modules/messaging.py diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py new file mode 100644 index 000000000..1457b1c0a --- /dev/null +++ b/SoftLayer/CLI/modules/messaging.py @@ -0,0 +1,302 @@ +""" +usage: sl messaging [] [...] [options] + +Manage SoftLayer Message Queue + +The available commands are: + account List all queue accounts + queue Queue-related commands + topic Topic-related commands +""" +# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. +# :license: BSD, see LICENSE for more details. +# from SoftLayer import NetworkManager +import sys + +from SoftLayer.CLI import CLIRunnable, Table +from SoftLayer.CLI.helpers import CLIAbort, listing, ArgumentError + +try: + import softlayer_messaging +except ImportError: + raise CLIAbort("""This functionality requires the softlayer_messaging package. +Run 'pip install softlayer_messaging' to install.""") + + +def get_mq_client(account_id, env): + client = softlayer_messaging.get_client(account_id) + client.authenticate(env.config.get('username'), env.config.get('api_key')) + return client + + +class ListAccounts(CLIRunnable): + """ +usage: sl messaging list-accounts [options] + +List SoftLayer Message Queue Accounts + +""" + action = 'list-accounts' + + @staticmethod + def execute(client, args): + accounts = client['Account'].getMessageQueueAccounts( + mask='id,name,status,nodes') + + t = Table([ + 'id', 'name', 'status' + ]) + for account in accounts: + t.add_row([ + account['nodes'][0]['accountName'], + account['name'], + account['status']['name'], + ]) + + return t + + +def queue_table(queue): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['name', queue['name']]) + t.add_row(['message_count', queue['message_count']]) + t.add_row(['visible_message_count', queue['visible_message_count']]) + t.add_row(['tags', listing(queue['tags'] or [])]) + t.add_row(['expiration', queue['expiration']]) + t.add_row(['visibility_interval', queue['visibility_interval']]) + return t + + +def message_table(message): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['id', message['id']]) + t.add_row(['initial_entry_time', message['initial_entry_time']]) + t.add_row(['visibility_delay', message['visibility_delay']]) + t.add_row(['visibility_interval', message['visibility_interval']]) + t.add_row(['fields', message['fields']]) + return [t, message['body']] + + +def topic_table(topic): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['name', topic['name']]) + t.add_row(['tags', listing(topic['tags'] or [])]) + return t + + +def subscription_table(sub): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['id', sub['id']]) + t.add_row(['endpoint_type', sub['endpoint_type']]) + for k, v in sub['endpoint'].items(): + t.add_row([k, v]) + return t + + +class Queue(CLIRunnable): + """ +usage: sl messaging queue list [options] + sl messaging queue detail [options] + sl messaging queue create [options] + sl messaging queue modify [options] + sl messaging queue delete [] [options] + sl messaging queue push ( | [-]) [options] + sl messaging queue pop [options] + +Manage queues + +Queue Create/modify Options: + --visibility_interval=SECONDS Time in seconds that messages will re-appear + after being popped + --expiration=SECONDS Time in seconds that messages will live + --tags=TAGS Comma-separated list of tags + +Queue Delete Options: + --force Flag to force the deletion of the queue even when there are messages + +Pop Options: + --count=NUM Count of messages to pop +""" + action = 'queue' + + @classmethod + def execute(cls, client, args): + mq_client = get_mq_client(args[''], cls.env) + + # list + if args['list']: + queues = mq_client.queues()['items'] + + t = Table([ + 'name', 'message_count', 'visible_message_count' + ]) + for queue in queues: + t.add_row([ + queue['name'], + queue['message_count'], + queue['visible_message_count'], + ]) + return t + # detail + elif args['detail']: + queue = mq_client.queue(args['']).detail() + return queue_table(queue) + # create + elif args['create'] or args['modify']: + tags = None + if args.get('--tags'): + tags = [tag.strip() for tag in args.get('--tags').split(',')] + + queue = mq_client.create_queue( + args[''], + visibility_interval=int(args.get('--visibility_interval') or 30), + expiration=int(args.get('--expiration') or 604800), + tags=tags, + ) + return queue_table(queue) + # delete + elif args['delete']: + if args['']: + messages = mq_client.queue(args['']).message( + args['']).delete() + else: + mq_client.queue(args['']).delete( + args.get('--force')) + # push message + elif args['push']: + # the message body comes from the positional argument or stdin + body = '' + if args[''] is not None: + body = args[''] + else: + body = sys.stdin.read() + return message_table( + mq_client.queue(args['']).push(body)) + # pop message + elif args['pop']: + messages = mq_client.queue(args['']).pop( + args.get('--count') or 1) + formatted_messages = [] + for message in messages['items']: + formatted_messages.append(message_table(message)) + return formatted_messages + else: + raise CLIAbort('Invalid command') + + +class Topic(CLIRunnable): + """ +usage: sl messaging topic list [options] + sl messaging topic detail [options] + sl messaging topic create [options] + sl messaging topic delete [] [options] + sl messaging topic push ( | [-]) [options] + +Manage topics and subscriptions + +Topic/Subscription Create Options: + --subscription Create a subscription + --type=TYPE Type of endpoint, [Options: http, queue] + --queue-name=NAME Queue name. Required if --type is queue + --http-method=METHOD HTTP Method to use if --type is http + --http-url=URL HTTP/HTTPS URL to use. Required if --type is http + --http-body=BODY HTTP Body template to use if --type is http + +Topic Delete Options: + --force Flag to force the deletion of the topic even when there are subscriptions + +""" + action = 'topic' + + @classmethod + def execute(cls, client, args): + mq_client = get_mq_client(args[''], cls.env) + + # list + if args['list']: + topics = mq_client.topics()['items'] + + t = Table(['name']) + for topic in topics: + t.add_row([topic['name']]) + return t + # detail + elif args['detail']: + topic = mq_client.topic(args['']).detail() + subscriptions = mq_client.topic(args['']).subscriptions() + tables = [] + for sub in subscriptions['items']: + tables.append(subscription_table(sub)) + return [topic_table(topic), tables] + # create + elif args['create']: + if args['--subscription']: + topic = mq_client.topic(args['']) + if args['--type'] == 'queue': + subscription = topic.create_subscription( + 'queue', + queue_name=args['--queue-name'], + ) + elif args['--type'] == 'http': + subscription = topic.create_subscription( + 'http', + method=args['--http-method'] or 'GET', + url=args['--http-url'], + body=args['--http-body'] + ) + else: + raise ArgumentError( + '--type should be either queue or http.') + return subscription_table(subscription) + else: + tags = None + if args.get('--tags'): + tags = [tag.strip() for tag in args.get('--tags').split(',')] + + topic = mq_client.create_topic( + args[''], + visibility_interval=int(args.get('--visibility_interval') or 30), + expiration=int(args.get('--expiration') or 604800), + tags=tags, + ) + return topic_table(topic) + # delete + elif args['delete']: + if args['']: + messages = mq_client.topic(args['']).subscription( + args['']).delete() + else: + mq_client.topic(args['']).delete( + args.get('--force')) + # push message + elif args['push']: + # the message body comes from the positional argument or stdin + body = '' + if args[''] is not None: + body = args[''] + else: + body = sys.stdin.read() + return message_table( + mq_client.topic(args['']).push(body)) + # pop message + elif args['pop']: + messages = mq_client.topic(args['']).pop( + args.get('--count') or 1) + formatted_messages = [] + for message in messages['items']: + formatted_messages.append(message_table(message)) + return formatted_messages + else: + raise CLIAbort('Invalid command') From d1e01a4a9b718cda8454eafda19e79a01f4c58fd Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 30 May 2013 15:41:52 -0500 Subject: [PATCH 0036/3227] Corrects reference to list-accounts --- SoftLayer/CLI/modules/messaging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py index 1457b1c0a..76c23fce4 100644 --- a/SoftLayer/CLI/modules/messaging.py +++ b/SoftLayer/CLI/modules/messaging.py @@ -4,9 +4,9 @@ Manage SoftLayer Message Queue The available commands are: - account List all queue accounts - queue Queue-related commands - topic Topic-related commands + list-accounts List all queue accounts + queue Queue-related commands + topic Topic-related commands """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. From 968f3e35657385c741c68de0052662341276bfcb Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 31 May 2013 17:08:32 -0500 Subject: [PATCH 0037/3227] Raises TypeError when you call an API method with invalid keyword arguments Before this, it's confusing whether the API arguments are keyword arguments or positional arguments. This should help that a bit. --- SoftLayer/API.py | 14 ++++++++++++++ SoftLayer/hardware.py | 3 +-- SoftLayer/tests/API/client_tests.py | 5 +++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index ac1cfa482..aefb660b4 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -15,6 +15,15 @@ API_USERNAME = None API_KEY = None API_BASE_URL = API_PUBLIC_ENDPOINT +VALID_CALL_ARGS = set([ + 'id', + 'mask', + 'filter', + 'headers', + 'raw_headers', + 'limit', + 'offset', +]) class AuthenticationBase(object): @@ -241,6 +250,11 @@ def call(self, service, method, *args, **kwargs): if kwargs.get('iter'): return self.iter_call(service, method, *args, **kwargs) + invalid_kwargs = set(kwargs.keys()) - VALID_CALL_ARGS + if invalid_kwargs: + raise TypeError( + 'Invalid keyword arguments: %s' % ','.join(invalid_kwargs)) + if not service.startswith(self._prefix): service = self._prefix + service diff --git a/SoftLayer/hardware.py b/SoftLayer/hardware.py index acdb8b58e..5a46048bd 100644 --- a/SoftLayer/hardware.py +++ b/SoftLayer/hardware.py @@ -118,8 +118,7 @@ def reload(self, id): """ return self.hardware.reloadCurrentOperatingSystemConfiguration( - id=id, - token='FORCE') + 'FORCE', id=id) def _get_ids_from_hostname(self, hostname): results = self.list_hardware(hostname=hostname, mask="id") diff --git a/SoftLayer/tests/API/client_tests.py b/SoftLayer/tests/API/client_tests.py index 5c37f2dd6..7d46f8103 100644 --- a/SoftLayer/tests/API/client_tests.py +++ b/SoftLayer/tests/API/client_tests.py @@ -340,6 +340,11 @@ def test_iter_call(self, _call): lambda: list(self.client.iter_call( 'SERVICE', 'METHOD', iter=True, chunk=0))) + def test_call_invalid_arguments(self): + self.assertRaises( + TypeError, + self.client.call, 'SERVICE', 'METHOD', invalid_kwarg='invalid') + class TestAuthenticationBase(unittest.TestCase): def test_get_headers(self): From b5909f04b90267e1f483e2c7a36e188b2e849e57 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 31 May 2013 17:16:21 -0500 Subject: [PATCH 0038/3227] Fixes Test --- SoftLayer/tests/API/hardware_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/API/hardware_tests.py index e415ec218..87109e8de 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/API/hardware_tests.py @@ -87,4 +87,4 @@ def test_get_instance(self): def test_reload(self): self.hardware.reload(id=1) f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration - f.assert_called_once_with(id=1, token='FORCE') + f.assert_called_once_with('FORCE', id=1) From f67d25f52757f5c4cf09896034460ca0dff14988 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 24 May 2013 09:55:07 -0500 Subject: [PATCH 0039/3227] Refactor Managers * Moves managers to SoftLayer.managers * Moves manager tests to SoftLayer.tests.managers * Adds missing manager imports so that all managers are exposed as SoftLayer.ManagerClass * Adds missing hardware manager to the docs * Adds Firewall Manager Test * Renames capital manager modules to lowercase * Moves manager exceptions to SoftLayer.exceptions * Adds a couple missing module doc blocks --- SoftLayer/CLI/modules/cci.py | 4 +- SoftLayer/CLI/modules/dns.py | 2 +- SoftLayer/CLI/modules/firewall.py | 2 +- SoftLayer/CLI/modules/hardware.py | 2 +- SoftLayer/CLI/modules/metadata.py | 2 +- SoftLayer/CLI/modules/ssl.py | 2 +- SoftLayer/__init__.py | 8 +-- SoftLayer/exceptions.py | 4 ++ SoftLayer/managers/__init__.py | 19 +++++ SoftLayer/{CCI.py => managers/cci.py} | 16 +---- SoftLayer/{DNS.py => managers/dns.py} | 9 +-- SoftLayer/{ => managers}/firewall.py | 29 ++++---- SoftLayer/{ => managers}/hardware.py | 9 +++ SoftLayer/{ => managers}/metadata.py | 1 - SoftLayer/{SSL.py => managers/ssl.py} | 1 - .../{API/client_tests.py => api_tests.py} | 17 +++-- SoftLayer/tests/{API => }/functional_tests.py | 4 +- SoftLayer/tests/{API => managers}/__init__.py | 0 .../tests/{API => managers}/cci_tests.py | 71 +++++++++---------- .../tests/{API => managers}/dns_tests.py | 48 +++++++------ SoftLayer/tests/managers/firewall_tests.py | 33 +++++++++ .../tests/{API => managers}/hardware_tests.py | 11 ++- .../tests/{API => managers}/metadata_tests.py | 31 ++++---- .../tests/{API => managers}/ssl_tests.py | 35 ++++----- SoftLayer/tests/{API => }/transport_tests.py | 7 ++ docs/api/managers.rst | 3 +- docs/api/managers/cci.rst | 2 +- docs/api/managers/dns.rst | 2 +- docs/api/managers/firewall.rst | 2 +- docs/api/managers/hardware.rst | 6 ++ docs/api/managers/metadata.rst | 4 +- docs/api/managers/ssl.rst | 2 +- docs/install.rst | 2 +- setup.py | 5 +- 34 files changed, 225 insertions(+), 170 deletions(-) create mode 100644 SoftLayer/managers/__init__.py rename SoftLayer/{CCI.py => managers/cci.py} (95%) rename SoftLayer/{DNS.py => managers/dns.py} (96%) rename SoftLayer/{ => managers}/firewall.py (55%) rename SoftLayer/{ => managers}/hardware.py (95%) rename SoftLayer/{ => managers}/metadata.py (99%) rename SoftLayer/{SSL.py => managers/ssl.py} (98%) rename SoftLayer/tests/{API/client_tests.py => api_tests.py} (99%) rename SoftLayer/tests/{API => }/functional_tests.py (97%) rename SoftLayer/tests/{API => managers}/__init__.py (100%) rename SoftLayer/tests/{API => managers}/cci_tests.py (87%) rename SoftLayer/tests/{API => managers}/dns_tests.py (65%) create mode 100644 SoftLayer/tests/managers/firewall_tests.py rename SoftLayer/tests/{API => managers}/hardware_tests.py (92%) rename SoftLayer/tests/{API => managers}/metadata_tests.py (77%) rename SoftLayer/tests/{API => managers}/ssl_tests.py (58%) rename SoftLayer/tests/{API => }/transport_tests.py (92%) create mode 100644 docs/api/managers/hardware.rst diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index babd02e44..b6503f4f4 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -25,7 +25,7 @@ from os import linesep import os.path -from SoftLayer.CCI import CCIManager +from SoftLayer import CCIManager from SoftLayer.CLI import ( CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, FormattedItem) @@ -693,7 +693,7 @@ def execute(cls, client, args): @staticmethod def dns_sync(client, args): - from SoftLayer.DNS import DNSManager, DNSZoneNotFound + from SoftLayer import DNSManager, DNSZoneNotFound dns = DNSManager(client) cci = CCIManager(client) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 9f208dff1..33b703e77 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -17,7 +17,7 @@ # :license: BSD, see LICENSE for more details. from SoftLayer.CLI import CLIRunnable, no_going_back, Table, CLIAbort -from SoftLayer.DNS import DNSManager, DNSZoneNotFound +from SoftLayer import DNSManager, DNSZoneNotFound class DumpZone(CLIRunnable): diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py index 37982b0f6..ef2b05721 100755 --- a/SoftLayer/CLI/modules/firewall.py +++ b/SoftLayer/CLI/modules/firewall.py @@ -11,7 +11,7 @@ from SoftLayer.CLI import CLIRunnable, Table, listing from SoftLayer.CLI.helpers import blank -from SoftLayer.firewall import FirewallManager +from SoftLayer import FirewallManager class FWList(CLIRunnable): diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index efadbbabc..1d040a3a3 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -15,7 +15,7 @@ from SoftLayer.CLI.helpers import ( CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, gb, no_going_back) -from SoftLayer.hardware import HardwareManager +from SoftLayer import HardwareManager def resolve_id(manager, identifier): diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index 9cc1a797b..f70757687 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -23,7 +23,7 @@ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. -from SoftLayer.metadata import MetadataManager +from SoftLayer import MetadataManager from SoftLayer.CLI import CLIRunnable, Table, listing, CLIAbort diff --git a/SoftLayer/CLI/modules/ssl.py b/SoftLayer/CLI/modules/ssl.py index 393655281..9013172b7 100755 --- a/SoftLayer/CLI/modules/ssl.py +++ b/SoftLayer/CLI/modules/ssl.py @@ -16,7 +16,7 @@ from SoftLayer.CLI.helpers import CLIRunnable, no_going_back, Table, CLIAbort from SoftLayer.CLI.helpers import blank -from SoftLayer.SSL import SSLManager +from SoftLayer import SSLManager class ListCerts(CLIRunnable): diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 6fb27cce5..3ff4ea1de 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -18,10 +18,7 @@ from API import ( Client, BasicAuthentication, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) -from DNS import DNSManager -from CCI import CCIManager -from metadata import MetadataManager -from hardware import HardwareManager +from managers import * # NOQA from SoftLayer.exceptions import * # NOQA __title__ = 'SoftLayer' @@ -30,5 +27,4 @@ __license__ = 'The BSD License' __copyright__ = 'Copyright 2013 SoftLayer Technologies, Inc.' __all__ = ['Client', 'BasicAuthentication', 'SoftLayerError', - 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', - 'DNSManager', 'CCIManager', 'MetadataManager', 'HardwareManager'] + 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] diff --git a/SoftLayer/exceptions.py b/SoftLayer/exceptions.py index 72d82f23c..6488ef0d6 100644 --- a/SoftLayer/exceptions.py +++ b/SoftLayer/exceptions.py @@ -78,3 +78,7 @@ class InvalidMethodParameters(ServerError): class InternalError(ServerError): pass + + +class DNSZoneNotFound(SoftLayerError): + pass diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py new file mode 100644 index 000000000..bb821caa6 --- /dev/null +++ b/SoftLayer/managers/__init__.py @@ -0,0 +1,19 @@ +""" + SoftLayer.managers + ~~~~~~~~~~~~~~~~~~ + 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. + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer.managers.cci import CCIManager +from SoftLayer.managers.dns import DNSManager +from SoftLayer.managers.firewall import FirewallManager +from SoftLayer.managers.hardware import HardwareManager +from SoftLayer.managers.metadata import MetadataManager +from SoftLayer.managers.ssl import SSLManager + +__all__ = ['CCIManager', 'DNSManager', 'FirewallManager', 'HardwareManager', + 'MetadataManager', 'SSLManager'] diff --git a/SoftLayer/CCI.py b/SoftLayer/managers/cci.py similarity index 95% rename from SoftLayer/CCI.py rename to SoftLayer/managers/cci.py index 068adf2eb..62ec47a59 100644 --- a/SoftLayer/CCI.py +++ b/SoftLayer/managers/cci.py @@ -10,20 +10,9 @@ from time import sleep from itertools import repeat -from SoftLayer.exceptions import SoftLayerError from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin -class CCICreateMissingRequired(SoftLayerError): - def __init__(self): - self.message = "cpu, memory, hostname, and domain are required" - - -class CCICreateMutuallyExclusive(SoftLayerError): - def __init__(self, *args): - self.message = "Can only specify one of:", ','.join(args) - - class CCIManager(IdentifierMixin, object): """ Manage CCIs """ def __init__(self, client): @@ -194,11 +183,12 @@ def _generate_create_dict( ] if not all(required): - raise CCICreateMissingRequired() + raise ValueError("cpu, memory, hostname, and domain are required") for me in mutually_exclusive: if all(me.values()): - raise CCICreateMutuallyExclusive(*me.keys()) + raise ValueError( + 'Can only specify one of: %s' % (','.join(me.keys()))) data = { "startCpus": int(cpus), diff --git a/SoftLayer/DNS.py b/SoftLayer/managers/dns.py similarity index 96% rename from SoftLayer/DNS.py rename to SoftLayer/managers/dns.py index 8bb1fcb6e..eb70fbad1 100644 --- a/SoftLayer/DNS.py +++ b/SoftLayer/managers/dns.py @@ -7,14 +7,7 @@ :license: BSD, see LICENSE for more details. """ from time import strftime -from SoftLayer.exceptions import SoftLayerError - - -__all__ = ["DNSZoneNotFound", "DNSManager"] - - -class DNSZoneNotFound(SoftLayerError): - pass +from SoftLayer.exceptions import DNSZoneNotFound class DNSManager(object): diff --git a/SoftLayer/firewall.py b/SoftLayer/managers/firewall.py similarity index 55% rename from SoftLayer/firewall.py rename to SoftLayer/managers/firewall.py index cd59b17c2..626605dd0 100644 --- a/SoftLayer/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -6,7 +6,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -__all__ = ['FirewallManager'] def has_firewall(vlan): @@ -29,16 +28,18 @@ def __init__(self, client): self.client = client def get_firewalls(self): - results = filter(has_firewall, self.client['Account'].getObject( - mask={'networkVlans': { - 'firewallNetworkComponents': None, - 'networkVlanFirewall': None, - 'dedicatedFirewallFlag': None, - 'firewallGuestNetworkComponents': None, - 'firewallInterfaces': {}, - 'firewallRules': None, - 'highAvailabilityFirewallFlag': None, - #'primarySubnet': None, - }})['networkVlans']) - - return results + results = self.client['Account'].getObject( + mask={ + 'networkVlans': { + 'firewallNetworkComponents': None, + 'networkVlanFirewall': None, + 'dedicatedFirewallFlag': None, + 'firewallGuestNetworkComponents': None, + 'firewallInterfaces': {}, + 'firewallRules': None, + 'highAvailabilityFirewallFlag': None, + #'primarySubnet': None, + } + })['networkVlans'] + + return filter(has_firewall, results) diff --git a/SoftLayer/hardware.py b/SoftLayer/managers/hardware.py similarity index 95% rename from SoftLayer/hardware.py rename to SoftLayer/managers/hardware.py index 5a46048bd..e350da5a6 100644 --- a/SoftLayer/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -1,3 +1,12 @@ +""" + SoftLayer.hardware + ~~~~~~~~~~~~~~~~~~ + Hardware Manager/helpers + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" + import socket from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin diff --git a/SoftLayer/metadata.py b/SoftLayer/managers/metadata.py similarity index 99% rename from SoftLayer/metadata.py rename to SoftLayer/managers/metadata.py index 9306d0eb6..e4b8435be 100644 --- a/SoftLayer/metadata.py +++ b/SoftLayer/managers/metadata.py @@ -10,7 +10,6 @@ from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST, USER_AGENT from SoftLayer.exceptions import SoftLayerAPIError, SoftLayerError -__all__ = ["MetadataManager"] METADATA_MAPPING = { 'backend_mac': {'call': 'BackendMacAddresses'}, diff --git a/SoftLayer/SSL.py b/SoftLayer/managers/ssl.py similarity index 98% rename from SoftLayer/SSL.py rename to SoftLayer/managers/ssl.py index 893cef292..2d4f4b9d3 100644 --- a/SoftLayer/SSL.py +++ b/SoftLayer/managers/ssl.py @@ -6,7 +6,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -__all__ = ["SSLManager"] class SSLManager(object): diff --git a/SoftLayer/tests/API/client_tests.py b/SoftLayer/tests/api_tests.py similarity index 99% rename from SoftLayer/tests/API/client_tests.py rename to SoftLayer/tests/api_tests.py index 7d46f8103..55eb7f0d4 100644 --- a/SoftLayer/tests/API/client_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -1,6 +1,6 @@ """ - SoftLayer.tests.API.client_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.api_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. @@ -112,11 +112,7 @@ def test_service_repr(self): self.assertIn("Service", repr(client['SERVICE'])) -class APICalls(unittest.TestCase): - def setUp(self): - self.client = SoftLayer.Client( - username='doesnotexist', api_key='issurelywrong', - endpoint_url="ENDPOINT") +class OldAPIClient(unittest.TestCase): @patch('SoftLayer.API.make_xml_rpc_api_call') def test_old_api(self, make_xml_rpc_api_call): @@ -181,6 +177,13 @@ def test_old_api_no_service(self): api_key='issurelywrong') self.assertRaises(SoftLayer.SoftLayerError, client.METHOD) + +class APIClient(unittest.TestCase): + def setUp(self): + self.client = SoftLayer.Client( + username='doesnotexist', api_key='issurelywrong', + endpoint_url="ENDPOINT") + @patch('SoftLayer.API.make_xml_rpc_api_call') def test_simple_call(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD() diff --git a/SoftLayer/tests/API/functional_tests.py b/SoftLayer/tests/functional_tests.py similarity index 97% rename from SoftLayer/tests/API/functional_tests.py rename to SoftLayer/tests/functional_tests.py index 245516eb3..1bc07d381 100644 --- a/SoftLayer/tests/API/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -1,6 +1,6 @@ """ - SoftLayer.tests.API.functional_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.functional_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. diff --git a/SoftLayer/tests/API/__init__.py b/SoftLayer/tests/managers/__init__.py similarity index 100% rename from SoftLayer/tests/API/__init__.py rename to SoftLayer/tests/managers/__init__.py diff --git a/SoftLayer/tests/API/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py similarity index 87% rename from SoftLayer/tests/API/cci_tests.py rename to SoftLayer/tests/managers/cci_tests.py index 2acd7f676..151c5364b 100644 --- a/SoftLayer/tests/API/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -1,29 +1,28 @@ """ - SoftLayer.tests.API.cci_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.cci_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer -import SoftLayer.CCI +from SoftLayer import CCIManager try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import MagicMock, ANY, call, patch -class CCITests_unittests(unittest.TestCase): +class CCITests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.cci = SoftLayer.CCIManager(self.client) + self.cci = CCIManager(self.client) def test_list_instances(self): mcall = call(mask=ANY, filter={}) - service = self.client.__getitem__() + service = self.client['Account'] self.cci.list_instances(hourly=True, monthly=True) service.getVirtualGuests.assert_has_calls(mcall) @@ -53,7 +52,7 @@ def test_list_instances_with_filters(self): private_ip='4.3.2.1', ) - service = self.client.__getitem__() + service = self.client['Account'] service.getVirtualGuests.assert_has_calls(call( filter={ 'virtualGuests': { @@ -77,13 +76,13 @@ def test_list_instances_with_filters(self): )) def test_resolve_ids_ip(self): - self.client.__getitem__().getVirtualGuests.return_value = \ - [{'id': '1234'}] + self.client['Account'].getVirtualGuests.return_value = [{'id': '1234'}] _id = self.cci._get_ids_from_ip('1.2.3.4') self.assertEqual(_id, ['1234']) - self.client.__getitem__().getVirtualGuests.side_effect = \ - [[], [{'id': '4321'}]] + self.client['Account'].getVirtualGuests.side_effect = [ + [], [{'id': '4321'}] + ] _id = self.cci._get_ids_from_ip('4.3.2.1') self.assertEqual(_id, ['4321']) @@ -91,51 +90,52 @@ def test_resolve_ids_ip(self): self.assertEqual(_id, []) def test_resolve_ids_hostname(self): - self.client.__getitem__().getVirtualGuests.return_value = \ + self.client['Account'].getVirtualGuests.return_value = \ [{'id': '1234'}] _id = self.cci._get_ids_from_hostname('hostname') self.assertEqual(_id, ['1234']) def test_get_instance(self): - self.client.__getitem__().getObject.return_value = { + self.client['Virtual_Guest'].getObject.return_value = { 'hourlyVirtualGuests': "this is unique"} self.cci.get_instance(1) - self.client.__getitem__().getObject.assert_called_once_with( + self.client['Virtual_Guest'].getObject.assert_called_once_with( id=1, mask=ANY) def test_get_create_options(self): self.cci.get_create_options() - f = self.client.__getitem__().getCreateObjectOptions + f = self.client['Virtual_Guest'].getCreateObjectOptions f.assert_called_once_with() def test_cancel_instance(self): self.cci.cancel_instance(id=1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + self.client['Virtual_Guest'].deleteObject.assert_called_once_with(id=1) def test_reload_instance(self): self.cci.reload_instance(id=1) - f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration + service = self.client['Virtual_Guest'] + f = service.reloadCurrentOperatingSystemConfiguration f.assert_called_once_with(id=1) - @patch('SoftLayer.CCI.CCIManager._generate_create_dict') + @patch('SoftLayer.managers.cci.CCIManager._generate_create_dict') def test_create_verify(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} self.cci.verify_create_instance(test=1, verify=1) create_dict.assert_called_once_with(test=1, verify=1) - f = self.client.__getitem__().generateOrderTemplate + f = self.client['Virtual_Guest'].generateOrderTemplate f.assert_called_once_with({'test': 1, 'verify': 1}) - @patch('SoftLayer.CCI.CCIManager._generate_create_dict') + @patch('SoftLayer.managers.cci.CCIManager._generate_create_dict') def test_create_instance(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} self.cci.create_instance(test=1, verify=1) create_dict.assert_called_once_with(test=1, verify=1) - self.client.__getitem__().createObject.assert_called_once_with( + self.client['Virtual_Guest'].createObject.assert_called_once_with( {'test': 1, 'verify': 1}) def test_generate_os_and_image(self): self.assertRaises( - SoftLayer.CCI.CCICreateMutuallyExclusive, + ValueError, self.cci._generate_create_dict, cpus=1, memory=1, @@ -146,15 +146,8 @@ def test_generate_os_and_image(self): ) def test_generate_missing(self): - self.assertRaises( - SoftLayer.CCI.CCICreateMissingRequired, - self.cci._generate_create_dict, - ) - self.assertRaises( - SoftLayer.CCI.CCICreateMissingRequired, - self.cci._generate_create_dict, - cpus=1 - ) + self.assertRaises(ValueError, self.cci._generate_create_dict) + self.assertRaises(ValueError, self.cci._generate_create_dict, cpus=1) def test_generate_basic(self): data = self.cci._generate_create_dict( @@ -381,7 +374,7 @@ def test_generate_single_disk(self): assert_data = { 'blockDevices': [ - {"device": "0", "diskImage":{"capacity": 50}}] + {"device": "0", "diskImage": {"capacity": 50}}] } self.assertTrue(data.get('blockDevices')) @@ -399,17 +392,17 @@ def test_generate_multi_disk(self): assert_data = { 'blockDevices': [ - {"device": "0", "diskImage":{"capacity": 50}}, - {"device": "2", "diskImage":{"capacity": 70}}, - {"device": "3", "diskImage":{"capacity": 100}}] + {"device": "0", "diskImage": {"capacity": 50}}, + {"device": "2", "diskImage": {"capacity": 70}}, + {"device": "3", "diskImage": {"capacity": 100}}] } self.assertTrue(data.get('blockDevices')) self.assertEqual(data['blockDevices'], assert_data['blockDevices']) - @patch('SoftLayer.CCI.sleep') + @patch('SoftLayer.managers.cci.sleep') def test_wait(self, _sleep): - guestObject = self.client.__getitem__().getObject + guestObject = self.client['Virtual_Guest'].getObject # test 4 iterations with positive match guestObject.side_effect = [ diff --git a/SoftLayer/tests/API/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py similarity index 65% rename from SoftLayer/tests/API/dns_tests.py rename to SoftLayer/tests/managers/dns_tests.py index 2de214d18..c80c19161 100644 --- a/SoftLayer/tests/API/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -1,16 +1,16 @@ """ - SoftLayer.tests.API.dns_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.dns_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer +from SoftLayer import DNSManager, DNSZoneNotFound try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import MagicMock, ANY @@ -18,7 +18,7 @@ class DNSTests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.dns_client = SoftLayer.DNSManager(self.client) + self.dns_client = DNSManager(self.client) def test_init_exercise(self): self.assertTrue(hasattr(self.dns_client, 'service')) @@ -26,7 +26,7 @@ def test_init_exercise(self): def test_list_zones(self): zone_list = ['test'] - self.client.__getitem__().getDomains.return_value = zone_list + self.client['Account'].getDomains.return_value = zone_list zones = self.dns_client.list_zones() self.assertEqual(zones, zone_list) @@ -37,42 +37,42 @@ def test_get_zone(self): ] # match - self.client.__getitem__().getByDomainName.return_value = \ - zone_list + self.client['Dns_Domain'].getByDomainName.return_value = zone_list res = self.dns_client.get_zone('example.com') self.assertEqual(res, zone_list[1]) # no match - from SoftLayer.DNS import DNSZoneNotFound self.assertRaises( DNSZoneNotFound, self.dns_client.get_zone, 'shouldnt-match.com') def test_create_zone(self): - self.client.__getitem__().createObject.return_value = \ - {'name': 'example.com'} + call = self.client['Dns_Domain'].createObject + call.return_value = {'name': 'example.com'} res = self.dns_client.create_zone('example.com') - self.client.__getitem__().createObject.assert_called_once_with( - {'name': 'example.com', "resourceRecords": {}, "serial": ANY}) + call.assert_called_once_with({ + 'name': 'example.com', "resourceRecords": {}, "serial": ANY + }) self.assertEqual(res, {'name': 'example.com'}) def test_delete_zone(self): self.dns_client.delete_zone(1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + self.client['Dns_Domain'].deleteObject.assert_called_once_with(id=1) def test_edit_zone(self): self.dns_client.edit_zone('example.com') - self.client.__getitem__().editObject.assert_called_once_with( + self.client['Dns_Domain'].editObject.assert_called_once_with( 'example.com') def test_create_record(self): self.dns_client.create_record(1, 'test', 'TXT', 'testing', ttl=1200) - self.client.__getitem__().createObject.assert_called_once_with( + f = self.client['Dns_Domain_ResourceRecord'].createObject + f.assert_called_once_with( { 'domainId': 1, 'ttl': 1200, @@ -83,10 +83,12 @@ def test_create_record(self): def test_delete_record(self): self.dns_client.delete_record(1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + f = self.client['Dns_Domain_ResourceRecord'].deleteObject + f.assert_called_once_with(id=1) def test_search_record(self): - self.client.__getitem__().getByDomainName.return_value = [{ + + self.client['Dns_Domain'].getByDomainName.return_value = [{ 'name': 'example.com', 'resourceRecords': [ {'host': 'TEST1'}, @@ -100,14 +102,14 @@ def test_search_record(self): def test_edit_record(self): self.dns_client.edit_record({'id': 1, 'name': 'test'}) - self.client.__getitem__().editObject.assert_called_once_with( + f = self.client['Dns_Domain_ResourceRecord'].editObject + f.assert_called_once_with( {'id': 1, 'name': 'test'}, id=1 ) def test_dump_zone(self): - self.client.__getitem__().getZoneFileContents.return_value = ( - 'lots of text') + f = self.client['Dns_Domain'].getZoneFileContents + f.return_value = 'lots of text' self.dns_client.dump_zone(1) - self.client.__getitem__().getZoneFileContents.assert_called_once_with( - id=1) + f.assert_called_once_with(id=1) diff --git a/SoftLayer/tests/managers/firewall_tests.py b/SoftLayer/tests/managers/firewall_tests.py new file mode 100644 index 000000000..e5a1c32a7 --- /dev/null +++ b/SoftLayer/tests/managers/firewall_tests.py @@ -0,0 +1,33 @@ +""" + SoftLayer.tests.managers.firewall_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer import FirewallManager + +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +from mock import MagicMock, ANY + + +class FirewallTests(unittest.TestCase): + + def setUp(self): + self.client = MagicMock() + self.firewall = FirewallManager(self.client) + + def test_get_firewalls(self): + vlan = { + 'dedicatedFirewallFlag': True, + } + call = self.client['Account'].getObject + call.return_value = {'networkVlans': [vlan]} + + firewalls = self.firewall.get_firewalls() + + self.assertEquals([vlan], firewalls) + call.assert_called_once_with(mask=ANY) diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py similarity index 92% rename from SoftLayer/tests/API/hardware_tests.py rename to SoftLayer/tests/managers/hardware_tests.py index 87109e8de..13aa2ec01 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -1,12 +1,11 @@ """ - SoftLayer.tests.API.hardware_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.hardware_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer -import SoftLayer.hardware +from SoftLayer import HardwareManager try: import unittest2 as unittest @@ -15,11 +14,11 @@ from mock import MagicMock, ANY, call -class HardwareTests_unittests(unittest.TestCase): +class HardwareTests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.hardware = SoftLayer.HardwareManager(self.client) + self.hardware = HardwareManager(self.client) def test_list_hardware(self): mcall = call(mask=ANY, filter={}) diff --git a/SoftLayer/tests/API/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py similarity index 77% rename from SoftLayer/tests/API/metadata_tests.py rename to SoftLayer/tests/managers/metadata_tests.py index df80d57b7..15ce5f6bf 100644 --- a/SoftLayer/tests/API/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -1,11 +1,12 @@ """ - SoftLayer.tests.API.metadata_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.metadata_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer +from SoftLayer import MetadataManager, SoftLayerError, SoftLayerAPIError +from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST try: import unittest2 as unittest @@ -17,7 +18,7 @@ class MetadataTests(unittest.TestCase): def setUp(self): - self.metadata = SoftLayer.MetadataManager() + self.metadata = MetadataManager() self.make_request = MagicMock() self.metadata.make_request = self.make_request @@ -46,11 +47,10 @@ def test_return_none(self): self.assertEqual(None, r) def test_w_param_error(self): - self.assertRaises(SoftLayer.SoftLayerError, self.metadata.get, 'vlans') + self.assertRaises(SoftLayerError, self.metadata.get, 'vlans') def test_not_exists(self): - self.assertRaises( - SoftLayer.SoftLayerError, self.metadata.get, 'something') + self.assertRaises(SoftLayerError, self.metadata.get, 'something') def test_networks_not_exist(self): self.make_request.return_value = [] @@ -80,13 +80,13 @@ def test_networks(self): class MetadataTestsMakeRequest(unittest.TestCase): def setUp(self): - self.metadata = SoftLayer.MetadataManager() + self.metadata = MetadataManager() self.url = '/'.join([ - SoftLayer.consts.API_PRIVATE_ENDPOINT_REST.rstrip('/'), + API_PRIVATE_ENDPOINT_REST.rstrip('/'), 'SoftLayer_Resource_Metadata', 'something.json']) - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_basic(self, make_api_call): r = self.metadata.make_request('something.json') make_api_call.assert_called_with( @@ -95,17 +95,16 @@ def test_basic(self, make_api_call): http_headers={'User-Agent': 'SoftLayer Python v2.2.0'}) self.assertEqual(make_api_call(), r) - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_raise_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError( + make_api_call.side_effect = SoftLayerAPIError( 'faultCode', 'faultString') self.assertRaises( - SoftLayer.SoftLayerAPIError, + SoftLayerAPIError, self.metadata.make_request, 'something.json') - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_raise_404_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError( - 404, 'faultString') + make_api_call.side_effect = SoftLayerAPIError(404, 'faultString') r = self.metadata.make_request('something.json') self.assertEqual(r, None) diff --git a/SoftLayer/tests/API/ssl_tests.py b/SoftLayer/tests/managers/ssl_tests.py similarity index 58% rename from SoftLayer/tests/API/ssl_tests.py rename to SoftLayer/tests/managers/ssl_tests.py index 5f887be4a..b8ddd2124 100644 --- a/SoftLayer/tests/API/ssl_tests.py +++ b/SoftLayer/tests/managers/ssl_tests.py @@ -1,11 +1,11 @@ """ - SoftLayer.tests.API.ssl_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.ssl_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -from SoftLayer.SSL import SSLManager +from SoftLayer import SSLManager try: import unittest2 as unittest @@ -14,7 +14,7 @@ from mock import MagicMock, ANY -class SSLTests_unittests(unittest.TestCase): +class SSLTests(unittest.TestCase): def setUp(self): self.client = MagicMock() @@ -23,16 +23,16 @@ def setUp(self): def test_list_certs(self): self.ssl.list_certs('valid') - self.client.__getitem__() \ - .getValidSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getValidSecurityCertificates + f.assert_called_once_with(mask=ANY) self.ssl.list_certs('expired') - self.client.__getitem__() \ - .getExpiredSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getExpiredSecurityCertificates + f.assert_called_once_with(mask=ANY) self.ssl.list_certs('all') - self.client.__getitem__() \ - .getSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getSecurityCertificates + f.assert_called_once_with(mask=ANY) def test_add_certificate(self): test_cert = { @@ -42,13 +42,13 @@ def test_add_certificate(self): self.ssl.add_certificate(test_cert) - self.client.__getitem__().createObject.assert_called_once_with( - test_cert) + f = self.client['Security_Certificate'].createObject + f.assert_called_once_with(test_cert) def test_remove_certificate(self): self.ssl.remove_certificate(self.test_id) - self.client.__getitem__() \ - .deleteObject.assert_called_once_with(id=self.test_id) + f = self.client['Security_Certificate'].deleteObject + f.assert_called_once_with(id=self.test_id) def test_edit_certificate(self): test_cert = { @@ -58,7 +58,8 @@ def test_edit_certificate(self): } self.ssl.edit_certificate(test_cert) - self.client.__getitem__().editObject.assert_called_once_with( + f = self.client['Security_Certificate'].editObject + f.assert_called_once_with( { 'id': self.test_id, 'certificate': 'cert', @@ -68,5 +69,5 @@ def test_edit_certificate(self): def test_get_certificate(self): self.ssl.get_certificate(self.test_id) - self.client.__getitem__().getObject.assert_called_once_with( - id=self.test_id) + f = self.client['Security_Certificate'].getObject + f.assert_called_once_with(id=self.test_id) diff --git a/SoftLayer/tests/API/transport_tests.py b/SoftLayer/tests/transport_tests.py similarity index 92% rename from SoftLayer/tests/API/transport_tests.py rename to SoftLayer/tests/transport_tests.py index 729f54d5b..f9ad470bd 100644 --- a/SoftLayer/tests/API/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -1,3 +1,10 @@ +""" + SoftLayer.tests.transport_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" try: import unittest2 as unittest except ImportError: diff --git a/docs/api/managers.rst b/docs/api/managers.rst index 1f866357a..c2bf1fe83 100644 --- a/docs/api/managers.rst +++ b/docs/api/managers.rst @@ -4,7 +4,8 @@ Managers -------- :: - >>> from SoftLayer.CCI import CCIManager + >>> from SoftLayer import CCIManager, Client + >>> client = Client(...) >>> cci = CCIManager(client) >>> cci.list_instances() [...] diff --git a/docs/api/managers/cci.rst b/docs/api/managers/cci.rst index 5af2a9a1d..d845c44cc 100644 --- a/docs/api/managers/cci.rst +++ b/docs/api/managers/cci.rst @@ -1,6 +1,6 @@ .. _cci: -.. automodule:: SoftLayer.CCI +.. automodule:: SoftLayer.managers.cci :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/dns.rst b/docs/api/managers/dns.rst index 047a1ea49..eef936177 100644 --- a/docs/api/managers/dns.rst +++ b/docs/api/managers/dns.rst @@ -1,6 +1,6 @@ .. _dns: -.. automodule:: SoftLayer.DNS +.. automodule:: SoftLayer.managers.dns :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/firewall.rst b/docs/api/managers/firewall.rst index ec4eda1a2..8bb5cd5b1 100644 --- a/docs/api/managers/firewall.rst +++ b/docs/api/managers/firewall.rst @@ -1,6 +1,6 @@ .. _firewall: -.. automodule:: SoftLayer.firewall +.. automodule:: SoftLayer.managers.firewall :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/hardware.rst b/docs/api/managers/hardware.rst new file mode 100644 index 000000000..6be2bbba8 --- /dev/null +++ b/docs/api/managers/hardware.rst @@ -0,0 +1,6 @@ +.. _hardware: + +.. automodule:: SoftLayer.managers.hardware + :members: + :inherited-members: + :undoc-members: diff --git a/docs/api/managers/metadata.rst b/docs/api/managers/metadata.rst index e5cb87b2d..e37ec86ca 100644 --- a/docs/api/managers/metadata.rst +++ b/docs/api/managers/metadata.rst @@ -1,8 +1,8 @@ .. _metadata: -.. automodule:: SoftLayer.metadata +.. automodule:: SoftLayer.managers.metadata :members: :inherited-members: :undoc-members: -.. autoattribute:: SoftLayer.metadata.METADATA_ATTRIBUTES \ No newline at end of file +.. autoattribute:: SoftLayer.managers.metadata.METADATA_ATTRIBUTES \ No newline at end of file diff --git a/docs/api/managers/ssl.rst b/docs/api/managers/ssl.rst index 17c9e053e..b1fb6dd1f 100644 --- a/docs/api/managers/ssl.rst +++ b/docs/api/managers/ssl.rst @@ -1,6 +1,6 @@ .. _ssl: -.. automodule:: SoftLayer.SSL +.. automodule:: SoftLayer.managers.ssl :members: :inherited-members: :undoc-members: diff --git a/docs/install.rst b/docs/install.rst index f9513a68b..cc6cd6d3a 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -25,7 +25,7 @@ The project is developed on GitHub, at `github.com/softlayer/softlayer-api-pytho You can clone the public repository:: - git clone git://github.com/softlayer/softlayer-api-python-client.git + $ git clone git://github.com/softlayer/softlayer-api-python-client.git Or, Download the `tarball `_:: diff --git a/setup.py b/setup.py index c0391e99a..e9992e716 100644 --- a/setup.py +++ b/setup.py @@ -47,9 +47,10 @@ 'SoftLayer', 'SoftLayer.CLI', 'SoftLayer.CLI.modules', + 'SoftLayer.managers', 'SoftLayer.tests', - 'SoftLayer.tests.API', - 'SoftLayer.tests.CLI' + 'SoftLayer.tests.CLI', + 'SoftLayer.tests.managers', ], license='The BSD License', zip_safe=False, From 84c64400b7772caffca78a54c51e7b6b67ebdf4f Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 24 May 2013 09:55:07 -0500 Subject: [PATCH 0040/3227] Refactor Managers * Moves managers to SoftLayer.managers * Moves manager tests to SoftLayer.tests.managers * Adds missing manager imports so that all managers are exposed as SoftLayer.ManagerClass * Adds missing hardware manager to the docs * Adds Firewall Manager Test * Renames capital manager modules to lowercase * Moves manager exceptions to SoftLayer.exceptions * Adds a couple missing module doc blocks --- SoftLayer/CLI/modules/cci.py | 4 +- SoftLayer/CLI/modules/dns.py | 2 +- SoftLayer/CLI/modules/firewall.py | 2 +- SoftLayer/CLI/modules/hardware.py | 2 +- SoftLayer/CLI/modules/metadata.py | 2 +- SoftLayer/CLI/modules/ssl.py | 2 +- SoftLayer/__init__.py | 8 +-- SoftLayer/exceptions.py | 4 ++ SoftLayer/managers/__init__.py | 19 +++++ SoftLayer/{CCI.py => managers/cci.py} | 16 +---- SoftLayer/{DNS.py => managers/dns.py} | 9 +-- SoftLayer/{ => managers}/firewall.py | 29 ++++---- SoftLayer/{ => managers}/hardware.py | 9 +++ SoftLayer/{ => managers}/metadata.py | 1 - SoftLayer/{SSL.py => managers/ssl.py} | 1 - .../{API/client_tests.py => api_tests.py} | 17 +++-- SoftLayer/tests/{API => }/functional_tests.py | 4 +- SoftLayer/tests/{API => managers}/__init__.py | 0 .../tests/{API => managers}/cci_tests.py | 71 +++++++++---------- .../tests/{API => managers}/dns_tests.py | 48 +++++++------ SoftLayer/tests/managers/firewall_tests.py | 33 +++++++++ .../tests/{API => managers}/hardware_tests.py | 11 ++- .../tests/{API => managers}/metadata_tests.py | 31 ++++---- .../tests/{API => managers}/ssl_tests.py | 35 ++++----- SoftLayer/tests/{API => }/transport_tests.py | 7 ++ docs/api/managers.rst | 3 +- docs/api/managers/cci.rst | 2 +- docs/api/managers/dns.rst | 2 +- docs/api/managers/firewall.rst | 2 +- docs/api/managers/hardware.rst | 6 ++ docs/api/managers/metadata.rst | 4 +- docs/api/managers/ssl.rst | 2 +- docs/install.rst | 2 +- setup.py | 5 +- 34 files changed, 225 insertions(+), 170 deletions(-) create mode 100644 SoftLayer/managers/__init__.py rename SoftLayer/{CCI.py => managers/cci.py} (95%) rename SoftLayer/{DNS.py => managers/dns.py} (96%) rename SoftLayer/{ => managers}/firewall.py (55%) rename SoftLayer/{ => managers}/hardware.py (95%) rename SoftLayer/{ => managers}/metadata.py (99%) rename SoftLayer/{SSL.py => managers/ssl.py} (98%) rename SoftLayer/tests/{API/client_tests.py => api_tests.py} (99%) rename SoftLayer/tests/{API => }/functional_tests.py (97%) rename SoftLayer/tests/{API => managers}/__init__.py (100%) rename SoftLayer/tests/{API => managers}/cci_tests.py (87%) rename SoftLayer/tests/{API => managers}/dns_tests.py (65%) create mode 100644 SoftLayer/tests/managers/firewall_tests.py rename SoftLayer/tests/{API => managers}/hardware_tests.py (92%) rename SoftLayer/tests/{API => managers}/metadata_tests.py (77%) rename SoftLayer/tests/{API => managers}/ssl_tests.py (58%) rename SoftLayer/tests/{API => }/transport_tests.py (92%) create mode 100644 docs/api/managers/hardware.rst diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index babd02e44..b6503f4f4 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -25,7 +25,7 @@ from os import linesep import os.path -from SoftLayer.CCI import CCIManager +from SoftLayer import CCIManager from SoftLayer.CLI import ( CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, FormattedItem) @@ -693,7 +693,7 @@ def execute(cls, client, args): @staticmethod def dns_sync(client, args): - from SoftLayer.DNS import DNSManager, DNSZoneNotFound + from SoftLayer import DNSManager, DNSZoneNotFound dns = DNSManager(client) cci = CCIManager(client) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 9f208dff1..33b703e77 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -17,7 +17,7 @@ # :license: BSD, see LICENSE for more details. from SoftLayer.CLI import CLIRunnable, no_going_back, Table, CLIAbort -from SoftLayer.DNS import DNSManager, DNSZoneNotFound +from SoftLayer import DNSManager, DNSZoneNotFound class DumpZone(CLIRunnable): diff --git a/SoftLayer/CLI/modules/firewall.py b/SoftLayer/CLI/modules/firewall.py index 37982b0f6..ef2b05721 100755 --- a/SoftLayer/CLI/modules/firewall.py +++ b/SoftLayer/CLI/modules/firewall.py @@ -11,7 +11,7 @@ from SoftLayer.CLI import CLIRunnable, Table, listing from SoftLayer.CLI.helpers import blank -from SoftLayer.firewall import FirewallManager +from SoftLayer import FirewallManager class FWList(CLIRunnable): diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index efadbbabc..1d040a3a3 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -15,7 +15,7 @@ from SoftLayer.CLI.helpers import ( CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, gb, no_going_back) -from SoftLayer.hardware import HardwareManager +from SoftLayer import HardwareManager def resolve_id(manager, identifier): diff --git a/SoftLayer/CLI/modules/metadata.py b/SoftLayer/CLI/modules/metadata.py index 9cc1a797b..f70757687 100644 --- a/SoftLayer/CLI/modules/metadata.py +++ b/SoftLayer/CLI/modules/metadata.py @@ -23,7 +23,7 @@ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. -from SoftLayer.metadata import MetadataManager +from SoftLayer import MetadataManager from SoftLayer.CLI import CLIRunnable, Table, listing, CLIAbort diff --git a/SoftLayer/CLI/modules/ssl.py b/SoftLayer/CLI/modules/ssl.py index 393655281..9013172b7 100755 --- a/SoftLayer/CLI/modules/ssl.py +++ b/SoftLayer/CLI/modules/ssl.py @@ -16,7 +16,7 @@ from SoftLayer.CLI.helpers import CLIRunnable, no_going_back, Table, CLIAbort from SoftLayer.CLI.helpers import blank -from SoftLayer.SSL import SSLManager +from SoftLayer import SSLManager class ListCerts(CLIRunnable): diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 6fb27cce5..3ff4ea1de 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -18,10 +18,7 @@ from API import ( Client, BasicAuthentication, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) -from DNS import DNSManager -from CCI import CCIManager -from metadata import MetadataManager -from hardware import HardwareManager +from managers import * # NOQA from SoftLayer.exceptions import * # NOQA __title__ = 'SoftLayer' @@ -30,5 +27,4 @@ __license__ = 'The BSD License' __copyright__ = 'Copyright 2013 SoftLayer Technologies, Inc.' __all__ = ['Client', 'BasicAuthentication', 'SoftLayerError', - 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', - 'DNSManager', 'CCIManager', 'MetadataManager', 'HardwareManager'] + 'SoftLayerAPIError', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] diff --git a/SoftLayer/exceptions.py b/SoftLayer/exceptions.py index 72d82f23c..6488ef0d6 100644 --- a/SoftLayer/exceptions.py +++ b/SoftLayer/exceptions.py @@ -78,3 +78,7 @@ class InvalidMethodParameters(ServerError): class InternalError(ServerError): pass + + +class DNSZoneNotFound(SoftLayerError): + pass diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py new file mode 100644 index 000000000..bb821caa6 --- /dev/null +++ b/SoftLayer/managers/__init__.py @@ -0,0 +1,19 @@ +""" + SoftLayer.managers + ~~~~~~~~~~~~~~~~~~ + 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. + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer.managers.cci import CCIManager +from SoftLayer.managers.dns import DNSManager +from SoftLayer.managers.firewall import FirewallManager +from SoftLayer.managers.hardware import HardwareManager +from SoftLayer.managers.metadata import MetadataManager +from SoftLayer.managers.ssl import SSLManager + +__all__ = ['CCIManager', 'DNSManager', 'FirewallManager', 'HardwareManager', + 'MetadataManager', 'SSLManager'] diff --git a/SoftLayer/CCI.py b/SoftLayer/managers/cci.py similarity index 95% rename from SoftLayer/CCI.py rename to SoftLayer/managers/cci.py index 068adf2eb..62ec47a59 100644 --- a/SoftLayer/CCI.py +++ b/SoftLayer/managers/cci.py @@ -10,20 +10,9 @@ from time import sleep from itertools import repeat -from SoftLayer.exceptions import SoftLayerError from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin -class CCICreateMissingRequired(SoftLayerError): - def __init__(self): - self.message = "cpu, memory, hostname, and domain are required" - - -class CCICreateMutuallyExclusive(SoftLayerError): - def __init__(self, *args): - self.message = "Can only specify one of:", ','.join(args) - - class CCIManager(IdentifierMixin, object): """ Manage CCIs """ def __init__(self, client): @@ -194,11 +183,12 @@ def _generate_create_dict( ] if not all(required): - raise CCICreateMissingRequired() + raise ValueError("cpu, memory, hostname, and domain are required") for me in mutually_exclusive: if all(me.values()): - raise CCICreateMutuallyExclusive(*me.keys()) + raise ValueError( + 'Can only specify one of: %s' % (','.join(me.keys()))) data = { "startCpus": int(cpus), diff --git a/SoftLayer/DNS.py b/SoftLayer/managers/dns.py similarity index 96% rename from SoftLayer/DNS.py rename to SoftLayer/managers/dns.py index 8bb1fcb6e..eb70fbad1 100644 --- a/SoftLayer/DNS.py +++ b/SoftLayer/managers/dns.py @@ -7,14 +7,7 @@ :license: BSD, see LICENSE for more details. """ from time import strftime -from SoftLayer.exceptions import SoftLayerError - - -__all__ = ["DNSZoneNotFound", "DNSManager"] - - -class DNSZoneNotFound(SoftLayerError): - pass +from SoftLayer.exceptions import DNSZoneNotFound class DNSManager(object): diff --git a/SoftLayer/firewall.py b/SoftLayer/managers/firewall.py similarity index 55% rename from SoftLayer/firewall.py rename to SoftLayer/managers/firewall.py index cd59b17c2..626605dd0 100644 --- a/SoftLayer/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -6,7 +6,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -__all__ = ['FirewallManager'] def has_firewall(vlan): @@ -29,16 +28,18 @@ def __init__(self, client): self.client = client def get_firewalls(self): - results = filter(has_firewall, self.client['Account'].getObject( - mask={'networkVlans': { - 'firewallNetworkComponents': None, - 'networkVlanFirewall': None, - 'dedicatedFirewallFlag': None, - 'firewallGuestNetworkComponents': None, - 'firewallInterfaces': {}, - 'firewallRules': None, - 'highAvailabilityFirewallFlag': None, - #'primarySubnet': None, - }})['networkVlans']) - - return results + results = self.client['Account'].getObject( + mask={ + 'networkVlans': { + 'firewallNetworkComponents': None, + 'networkVlanFirewall': None, + 'dedicatedFirewallFlag': None, + 'firewallGuestNetworkComponents': None, + 'firewallInterfaces': {}, + 'firewallRules': None, + 'highAvailabilityFirewallFlag': None, + #'primarySubnet': None, + } + })['networkVlans'] + + return filter(has_firewall, results) diff --git a/SoftLayer/hardware.py b/SoftLayer/managers/hardware.py similarity index 95% rename from SoftLayer/hardware.py rename to SoftLayer/managers/hardware.py index 5a46048bd..e350da5a6 100644 --- a/SoftLayer/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -1,3 +1,12 @@ +""" + SoftLayer.hardware + ~~~~~~~~~~~~~~~~~~ + Hardware Manager/helpers + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" + import socket from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin diff --git a/SoftLayer/metadata.py b/SoftLayer/managers/metadata.py similarity index 99% rename from SoftLayer/metadata.py rename to SoftLayer/managers/metadata.py index 9306d0eb6..e4b8435be 100644 --- a/SoftLayer/metadata.py +++ b/SoftLayer/managers/metadata.py @@ -10,7 +10,6 @@ from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST, USER_AGENT from SoftLayer.exceptions import SoftLayerAPIError, SoftLayerError -__all__ = ["MetadataManager"] METADATA_MAPPING = { 'backend_mac': {'call': 'BackendMacAddresses'}, diff --git a/SoftLayer/SSL.py b/SoftLayer/managers/ssl.py similarity index 98% rename from SoftLayer/SSL.py rename to SoftLayer/managers/ssl.py index 893cef292..2d4f4b9d3 100644 --- a/SoftLayer/SSL.py +++ b/SoftLayer/managers/ssl.py @@ -6,7 +6,6 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -__all__ = ["SSLManager"] class SSLManager(object): diff --git a/SoftLayer/tests/API/client_tests.py b/SoftLayer/tests/api_tests.py similarity index 99% rename from SoftLayer/tests/API/client_tests.py rename to SoftLayer/tests/api_tests.py index 7d46f8103..55eb7f0d4 100644 --- a/SoftLayer/tests/API/client_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -1,6 +1,6 @@ """ - SoftLayer.tests.API.client_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.api_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. @@ -112,11 +112,7 @@ def test_service_repr(self): self.assertIn("Service", repr(client['SERVICE'])) -class APICalls(unittest.TestCase): - def setUp(self): - self.client = SoftLayer.Client( - username='doesnotexist', api_key='issurelywrong', - endpoint_url="ENDPOINT") +class OldAPIClient(unittest.TestCase): @patch('SoftLayer.API.make_xml_rpc_api_call') def test_old_api(self, make_xml_rpc_api_call): @@ -181,6 +177,13 @@ def test_old_api_no_service(self): api_key='issurelywrong') self.assertRaises(SoftLayer.SoftLayerError, client.METHOD) + +class APIClient(unittest.TestCase): + def setUp(self): + self.client = SoftLayer.Client( + username='doesnotexist', api_key='issurelywrong', + endpoint_url="ENDPOINT") + @patch('SoftLayer.API.make_xml_rpc_api_call') def test_simple_call(self, make_xml_rpc_api_call): self.client['SERVICE'].METHOD() diff --git a/SoftLayer/tests/API/functional_tests.py b/SoftLayer/tests/functional_tests.py similarity index 97% rename from SoftLayer/tests/API/functional_tests.py rename to SoftLayer/tests/functional_tests.py index 245516eb3..1bc07d381 100644 --- a/SoftLayer/tests/API/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -1,6 +1,6 @@ """ - SoftLayer.tests.API.functional_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.functional_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. diff --git a/SoftLayer/tests/API/__init__.py b/SoftLayer/tests/managers/__init__.py similarity index 100% rename from SoftLayer/tests/API/__init__.py rename to SoftLayer/tests/managers/__init__.py diff --git a/SoftLayer/tests/API/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py similarity index 87% rename from SoftLayer/tests/API/cci_tests.py rename to SoftLayer/tests/managers/cci_tests.py index 2acd7f676..151c5364b 100644 --- a/SoftLayer/tests/API/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -1,29 +1,28 @@ """ - SoftLayer.tests.API.cci_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.cci_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer -import SoftLayer.CCI +from SoftLayer import CCIManager try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import MagicMock, ANY, call, patch -class CCITests_unittests(unittest.TestCase): +class CCITests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.cci = SoftLayer.CCIManager(self.client) + self.cci = CCIManager(self.client) def test_list_instances(self): mcall = call(mask=ANY, filter={}) - service = self.client.__getitem__() + service = self.client['Account'] self.cci.list_instances(hourly=True, monthly=True) service.getVirtualGuests.assert_has_calls(mcall) @@ -53,7 +52,7 @@ def test_list_instances_with_filters(self): private_ip='4.3.2.1', ) - service = self.client.__getitem__() + service = self.client['Account'] service.getVirtualGuests.assert_has_calls(call( filter={ 'virtualGuests': { @@ -77,13 +76,13 @@ def test_list_instances_with_filters(self): )) def test_resolve_ids_ip(self): - self.client.__getitem__().getVirtualGuests.return_value = \ - [{'id': '1234'}] + self.client['Account'].getVirtualGuests.return_value = [{'id': '1234'}] _id = self.cci._get_ids_from_ip('1.2.3.4') self.assertEqual(_id, ['1234']) - self.client.__getitem__().getVirtualGuests.side_effect = \ - [[], [{'id': '4321'}]] + self.client['Account'].getVirtualGuests.side_effect = [ + [], [{'id': '4321'}] + ] _id = self.cci._get_ids_from_ip('4.3.2.1') self.assertEqual(_id, ['4321']) @@ -91,51 +90,52 @@ def test_resolve_ids_ip(self): self.assertEqual(_id, []) def test_resolve_ids_hostname(self): - self.client.__getitem__().getVirtualGuests.return_value = \ + self.client['Account'].getVirtualGuests.return_value = \ [{'id': '1234'}] _id = self.cci._get_ids_from_hostname('hostname') self.assertEqual(_id, ['1234']) def test_get_instance(self): - self.client.__getitem__().getObject.return_value = { + self.client['Virtual_Guest'].getObject.return_value = { 'hourlyVirtualGuests': "this is unique"} self.cci.get_instance(1) - self.client.__getitem__().getObject.assert_called_once_with( + self.client['Virtual_Guest'].getObject.assert_called_once_with( id=1, mask=ANY) def test_get_create_options(self): self.cci.get_create_options() - f = self.client.__getitem__().getCreateObjectOptions + f = self.client['Virtual_Guest'].getCreateObjectOptions f.assert_called_once_with() def test_cancel_instance(self): self.cci.cancel_instance(id=1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + self.client['Virtual_Guest'].deleteObject.assert_called_once_with(id=1) def test_reload_instance(self): self.cci.reload_instance(id=1) - f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration + service = self.client['Virtual_Guest'] + f = service.reloadCurrentOperatingSystemConfiguration f.assert_called_once_with(id=1) - @patch('SoftLayer.CCI.CCIManager._generate_create_dict') + @patch('SoftLayer.managers.cci.CCIManager._generate_create_dict') def test_create_verify(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} self.cci.verify_create_instance(test=1, verify=1) create_dict.assert_called_once_with(test=1, verify=1) - f = self.client.__getitem__().generateOrderTemplate + f = self.client['Virtual_Guest'].generateOrderTemplate f.assert_called_once_with({'test': 1, 'verify': 1}) - @patch('SoftLayer.CCI.CCIManager._generate_create_dict') + @patch('SoftLayer.managers.cci.CCIManager._generate_create_dict') def test_create_instance(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} self.cci.create_instance(test=1, verify=1) create_dict.assert_called_once_with(test=1, verify=1) - self.client.__getitem__().createObject.assert_called_once_with( + self.client['Virtual_Guest'].createObject.assert_called_once_with( {'test': 1, 'verify': 1}) def test_generate_os_and_image(self): self.assertRaises( - SoftLayer.CCI.CCICreateMutuallyExclusive, + ValueError, self.cci._generate_create_dict, cpus=1, memory=1, @@ -146,15 +146,8 @@ def test_generate_os_and_image(self): ) def test_generate_missing(self): - self.assertRaises( - SoftLayer.CCI.CCICreateMissingRequired, - self.cci._generate_create_dict, - ) - self.assertRaises( - SoftLayer.CCI.CCICreateMissingRequired, - self.cci._generate_create_dict, - cpus=1 - ) + self.assertRaises(ValueError, self.cci._generate_create_dict) + self.assertRaises(ValueError, self.cci._generate_create_dict, cpus=1) def test_generate_basic(self): data = self.cci._generate_create_dict( @@ -381,7 +374,7 @@ def test_generate_single_disk(self): assert_data = { 'blockDevices': [ - {"device": "0", "diskImage":{"capacity": 50}}] + {"device": "0", "diskImage": {"capacity": 50}}] } self.assertTrue(data.get('blockDevices')) @@ -399,17 +392,17 @@ def test_generate_multi_disk(self): assert_data = { 'blockDevices': [ - {"device": "0", "diskImage":{"capacity": 50}}, - {"device": "2", "diskImage":{"capacity": 70}}, - {"device": "3", "diskImage":{"capacity": 100}}] + {"device": "0", "diskImage": {"capacity": 50}}, + {"device": "2", "diskImage": {"capacity": 70}}, + {"device": "3", "diskImage": {"capacity": 100}}] } self.assertTrue(data.get('blockDevices')) self.assertEqual(data['blockDevices'], assert_data['blockDevices']) - @patch('SoftLayer.CCI.sleep') + @patch('SoftLayer.managers.cci.sleep') def test_wait(self, _sleep): - guestObject = self.client.__getitem__().getObject + guestObject = self.client['Virtual_Guest'].getObject # test 4 iterations with positive match guestObject.side_effect = [ diff --git a/SoftLayer/tests/API/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py similarity index 65% rename from SoftLayer/tests/API/dns_tests.py rename to SoftLayer/tests/managers/dns_tests.py index 2de214d18..c80c19161 100644 --- a/SoftLayer/tests/API/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -1,16 +1,16 @@ """ - SoftLayer.tests.API.dns_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.dns_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer +from SoftLayer import DNSManager, DNSZoneNotFound try: import unittest2 as unittest except ImportError: - import unittest # NOQA + import unittest # NOQA from mock import MagicMock, ANY @@ -18,7 +18,7 @@ class DNSTests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.dns_client = SoftLayer.DNSManager(self.client) + self.dns_client = DNSManager(self.client) def test_init_exercise(self): self.assertTrue(hasattr(self.dns_client, 'service')) @@ -26,7 +26,7 @@ def test_init_exercise(self): def test_list_zones(self): zone_list = ['test'] - self.client.__getitem__().getDomains.return_value = zone_list + self.client['Account'].getDomains.return_value = zone_list zones = self.dns_client.list_zones() self.assertEqual(zones, zone_list) @@ -37,42 +37,42 @@ def test_get_zone(self): ] # match - self.client.__getitem__().getByDomainName.return_value = \ - zone_list + self.client['Dns_Domain'].getByDomainName.return_value = zone_list res = self.dns_client.get_zone('example.com') self.assertEqual(res, zone_list[1]) # no match - from SoftLayer.DNS import DNSZoneNotFound self.assertRaises( DNSZoneNotFound, self.dns_client.get_zone, 'shouldnt-match.com') def test_create_zone(self): - self.client.__getitem__().createObject.return_value = \ - {'name': 'example.com'} + call = self.client['Dns_Domain'].createObject + call.return_value = {'name': 'example.com'} res = self.dns_client.create_zone('example.com') - self.client.__getitem__().createObject.assert_called_once_with( - {'name': 'example.com', "resourceRecords": {}, "serial": ANY}) + call.assert_called_once_with({ + 'name': 'example.com', "resourceRecords": {}, "serial": ANY + }) self.assertEqual(res, {'name': 'example.com'}) def test_delete_zone(self): self.dns_client.delete_zone(1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + self.client['Dns_Domain'].deleteObject.assert_called_once_with(id=1) def test_edit_zone(self): self.dns_client.edit_zone('example.com') - self.client.__getitem__().editObject.assert_called_once_with( + self.client['Dns_Domain'].editObject.assert_called_once_with( 'example.com') def test_create_record(self): self.dns_client.create_record(1, 'test', 'TXT', 'testing', ttl=1200) - self.client.__getitem__().createObject.assert_called_once_with( + f = self.client['Dns_Domain_ResourceRecord'].createObject + f.assert_called_once_with( { 'domainId': 1, 'ttl': 1200, @@ -83,10 +83,12 @@ def test_create_record(self): def test_delete_record(self): self.dns_client.delete_record(1) - self.client.__getitem__().deleteObject.assert_called_once_with(id=1) + f = self.client['Dns_Domain_ResourceRecord'].deleteObject + f.assert_called_once_with(id=1) def test_search_record(self): - self.client.__getitem__().getByDomainName.return_value = [{ + + self.client['Dns_Domain'].getByDomainName.return_value = [{ 'name': 'example.com', 'resourceRecords': [ {'host': 'TEST1'}, @@ -100,14 +102,14 @@ def test_search_record(self): def test_edit_record(self): self.dns_client.edit_record({'id': 1, 'name': 'test'}) - self.client.__getitem__().editObject.assert_called_once_with( + f = self.client['Dns_Domain_ResourceRecord'].editObject + f.assert_called_once_with( {'id': 1, 'name': 'test'}, id=1 ) def test_dump_zone(self): - self.client.__getitem__().getZoneFileContents.return_value = ( - 'lots of text') + f = self.client['Dns_Domain'].getZoneFileContents + f.return_value = 'lots of text' self.dns_client.dump_zone(1) - self.client.__getitem__().getZoneFileContents.assert_called_once_with( - id=1) + f.assert_called_once_with(id=1) diff --git a/SoftLayer/tests/managers/firewall_tests.py b/SoftLayer/tests/managers/firewall_tests.py new file mode 100644 index 000000000..e5a1c32a7 --- /dev/null +++ b/SoftLayer/tests/managers/firewall_tests.py @@ -0,0 +1,33 @@ +""" + SoftLayer.tests.managers.firewall_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer import FirewallManager + +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +from mock import MagicMock, ANY + + +class FirewallTests(unittest.TestCase): + + def setUp(self): + self.client = MagicMock() + self.firewall = FirewallManager(self.client) + + def test_get_firewalls(self): + vlan = { + 'dedicatedFirewallFlag': True, + } + call = self.client['Account'].getObject + call.return_value = {'networkVlans': [vlan]} + + firewalls = self.firewall.get_firewalls() + + self.assertEquals([vlan], firewalls) + call.assert_called_once_with(mask=ANY) diff --git a/SoftLayer/tests/API/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py similarity index 92% rename from SoftLayer/tests/API/hardware_tests.py rename to SoftLayer/tests/managers/hardware_tests.py index 87109e8de..13aa2ec01 100644 --- a/SoftLayer/tests/API/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -1,12 +1,11 @@ """ - SoftLayer.tests.API.hardware_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.hardware_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer -import SoftLayer.hardware +from SoftLayer import HardwareManager try: import unittest2 as unittest @@ -15,11 +14,11 @@ from mock import MagicMock, ANY, call -class HardwareTests_unittests(unittest.TestCase): +class HardwareTests(unittest.TestCase): def setUp(self): self.client = MagicMock() - self.hardware = SoftLayer.HardwareManager(self.client) + self.hardware = HardwareManager(self.client) def test_list_hardware(self): mcall = call(mask=ANY, filter={}) diff --git a/SoftLayer/tests/API/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py similarity index 77% rename from SoftLayer/tests/API/metadata_tests.py rename to SoftLayer/tests/managers/metadata_tests.py index df80d57b7..15ce5f6bf 100644 --- a/SoftLayer/tests/API/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -1,11 +1,12 @@ """ - SoftLayer.tests.API.metadata_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.metadata_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import SoftLayer +from SoftLayer import MetadataManager, SoftLayerError, SoftLayerAPIError +from SoftLayer.consts import API_PRIVATE_ENDPOINT_REST try: import unittest2 as unittest @@ -17,7 +18,7 @@ class MetadataTests(unittest.TestCase): def setUp(self): - self.metadata = SoftLayer.MetadataManager() + self.metadata = MetadataManager() self.make_request = MagicMock() self.metadata.make_request = self.make_request @@ -46,11 +47,10 @@ def test_return_none(self): self.assertEqual(None, r) def test_w_param_error(self): - self.assertRaises(SoftLayer.SoftLayerError, self.metadata.get, 'vlans') + self.assertRaises(SoftLayerError, self.metadata.get, 'vlans') def test_not_exists(self): - self.assertRaises( - SoftLayer.SoftLayerError, self.metadata.get, 'something') + self.assertRaises(SoftLayerError, self.metadata.get, 'something') def test_networks_not_exist(self): self.make_request.return_value = [] @@ -80,13 +80,13 @@ def test_networks(self): class MetadataTestsMakeRequest(unittest.TestCase): def setUp(self): - self.metadata = SoftLayer.MetadataManager() + self.metadata = MetadataManager() self.url = '/'.join([ - SoftLayer.consts.API_PRIVATE_ENDPOINT_REST.rstrip('/'), + API_PRIVATE_ENDPOINT_REST.rstrip('/'), 'SoftLayer_Resource_Metadata', 'something.json']) - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_basic(self, make_api_call): r = self.metadata.make_request('something.json') make_api_call.assert_called_with( @@ -95,17 +95,16 @@ def test_basic(self, make_api_call): http_headers={'User-Agent': 'SoftLayer Python v2.2.0'}) self.assertEqual(make_api_call(), r) - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_raise_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError( + make_api_call.side_effect = SoftLayerAPIError( 'faultCode', 'faultString') self.assertRaises( - SoftLayer.SoftLayerAPIError, + SoftLayerAPIError, self.metadata.make_request, 'something.json') - @patch('SoftLayer.metadata.make_rest_api_call') + @patch('SoftLayer.managers.metadata.make_rest_api_call') def test_raise_404_error(self, make_api_call): - make_api_call.side_effect = SoftLayer.SoftLayerAPIError( - 404, 'faultString') + make_api_call.side_effect = SoftLayerAPIError(404, 'faultString') r = self.metadata.make_request('something.json') self.assertEqual(r, None) diff --git a/SoftLayer/tests/API/ssl_tests.py b/SoftLayer/tests/managers/ssl_tests.py similarity index 58% rename from SoftLayer/tests/API/ssl_tests.py rename to SoftLayer/tests/managers/ssl_tests.py index 5f887be4a..b8ddd2124 100644 --- a/SoftLayer/tests/API/ssl_tests.py +++ b/SoftLayer/tests/managers/ssl_tests.py @@ -1,11 +1,11 @@ """ - SoftLayer.tests.API.ssl_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.managers.ssl_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -from SoftLayer.SSL import SSLManager +from SoftLayer import SSLManager try: import unittest2 as unittest @@ -14,7 +14,7 @@ from mock import MagicMock, ANY -class SSLTests_unittests(unittest.TestCase): +class SSLTests(unittest.TestCase): def setUp(self): self.client = MagicMock() @@ -23,16 +23,16 @@ def setUp(self): def test_list_certs(self): self.ssl.list_certs('valid') - self.client.__getitem__() \ - .getValidSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getValidSecurityCertificates + f.assert_called_once_with(mask=ANY) self.ssl.list_certs('expired') - self.client.__getitem__() \ - .getExpiredSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getExpiredSecurityCertificates + f.assert_called_once_with(mask=ANY) self.ssl.list_certs('all') - self.client.__getitem__() \ - .getSecurityCertificates.assert_called_once_with(mask=ANY) + f = self.client['Account'].getSecurityCertificates + f.assert_called_once_with(mask=ANY) def test_add_certificate(self): test_cert = { @@ -42,13 +42,13 @@ def test_add_certificate(self): self.ssl.add_certificate(test_cert) - self.client.__getitem__().createObject.assert_called_once_with( - test_cert) + f = self.client['Security_Certificate'].createObject + f.assert_called_once_with(test_cert) def test_remove_certificate(self): self.ssl.remove_certificate(self.test_id) - self.client.__getitem__() \ - .deleteObject.assert_called_once_with(id=self.test_id) + f = self.client['Security_Certificate'].deleteObject + f.assert_called_once_with(id=self.test_id) def test_edit_certificate(self): test_cert = { @@ -58,7 +58,8 @@ def test_edit_certificate(self): } self.ssl.edit_certificate(test_cert) - self.client.__getitem__().editObject.assert_called_once_with( + f = self.client['Security_Certificate'].editObject + f.assert_called_once_with( { 'id': self.test_id, 'certificate': 'cert', @@ -68,5 +69,5 @@ def test_edit_certificate(self): def test_get_certificate(self): self.ssl.get_certificate(self.test_id) - self.client.__getitem__().getObject.assert_called_once_with( - id=self.test_id) + f = self.client['Security_Certificate'].getObject + f.assert_called_once_with(id=self.test_id) diff --git a/SoftLayer/tests/API/transport_tests.py b/SoftLayer/tests/transport_tests.py similarity index 92% rename from SoftLayer/tests/API/transport_tests.py rename to SoftLayer/tests/transport_tests.py index 729f54d5b..f9ad470bd 100644 --- a/SoftLayer/tests/API/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -1,3 +1,10 @@ +""" + SoftLayer.tests.transport_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" try: import unittest2 as unittest except ImportError: diff --git a/docs/api/managers.rst b/docs/api/managers.rst index 1f866357a..c2bf1fe83 100644 --- a/docs/api/managers.rst +++ b/docs/api/managers.rst @@ -4,7 +4,8 @@ Managers -------- :: - >>> from SoftLayer.CCI import CCIManager + >>> from SoftLayer import CCIManager, Client + >>> client = Client(...) >>> cci = CCIManager(client) >>> cci.list_instances() [...] diff --git a/docs/api/managers/cci.rst b/docs/api/managers/cci.rst index 5af2a9a1d..d845c44cc 100644 --- a/docs/api/managers/cci.rst +++ b/docs/api/managers/cci.rst @@ -1,6 +1,6 @@ .. _cci: -.. automodule:: SoftLayer.CCI +.. automodule:: SoftLayer.managers.cci :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/dns.rst b/docs/api/managers/dns.rst index 047a1ea49..eef936177 100644 --- a/docs/api/managers/dns.rst +++ b/docs/api/managers/dns.rst @@ -1,6 +1,6 @@ .. _dns: -.. automodule:: SoftLayer.DNS +.. automodule:: SoftLayer.managers.dns :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/firewall.rst b/docs/api/managers/firewall.rst index ec4eda1a2..8bb5cd5b1 100644 --- a/docs/api/managers/firewall.rst +++ b/docs/api/managers/firewall.rst @@ -1,6 +1,6 @@ .. _firewall: -.. automodule:: SoftLayer.firewall +.. automodule:: SoftLayer.managers.firewall :members: :inherited-members: :undoc-members: diff --git a/docs/api/managers/hardware.rst b/docs/api/managers/hardware.rst new file mode 100644 index 000000000..6be2bbba8 --- /dev/null +++ b/docs/api/managers/hardware.rst @@ -0,0 +1,6 @@ +.. _hardware: + +.. automodule:: SoftLayer.managers.hardware + :members: + :inherited-members: + :undoc-members: diff --git a/docs/api/managers/metadata.rst b/docs/api/managers/metadata.rst index e5cb87b2d..e37ec86ca 100644 --- a/docs/api/managers/metadata.rst +++ b/docs/api/managers/metadata.rst @@ -1,8 +1,8 @@ .. _metadata: -.. automodule:: SoftLayer.metadata +.. automodule:: SoftLayer.managers.metadata :members: :inherited-members: :undoc-members: -.. autoattribute:: SoftLayer.metadata.METADATA_ATTRIBUTES \ No newline at end of file +.. autoattribute:: SoftLayer.managers.metadata.METADATA_ATTRIBUTES \ No newline at end of file diff --git a/docs/api/managers/ssl.rst b/docs/api/managers/ssl.rst index 17c9e053e..b1fb6dd1f 100644 --- a/docs/api/managers/ssl.rst +++ b/docs/api/managers/ssl.rst @@ -1,6 +1,6 @@ .. _ssl: -.. automodule:: SoftLayer.SSL +.. automodule:: SoftLayer.managers.ssl :members: :inherited-members: :undoc-members: diff --git a/docs/install.rst b/docs/install.rst index f9513a68b..cc6cd6d3a 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -25,7 +25,7 @@ The project is developed on GitHub, at `github.com/softlayer/softlayer-api-pytho You can clone the public repository:: - git clone git://github.com/softlayer/softlayer-api-python-client.git + $ git clone git://github.com/softlayer/softlayer-api-python-client.git Or, Download the `tarball `_:: diff --git a/setup.py b/setup.py index c0391e99a..e9992e716 100644 --- a/setup.py +++ b/setup.py @@ -47,9 +47,10 @@ 'SoftLayer', 'SoftLayer.CLI', 'SoftLayer.CLI.modules', + 'SoftLayer.managers', 'SoftLayer.tests', - 'SoftLayer.tests.API', - 'SoftLayer.tests.CLI' + 'SoftLayer.tests.CLI', + 'SoftLayer.tests.managers', ], license='The BSD License', zip_safe=False, From 6efbfacd4d61174afe4325ec4e7dfe9521106380 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 28 May 2013 11:08:35 -0500 Subject: [PATCH 0041/3227] First Attempt at Message Queue Module --- SoftLayer/CLI/modules/messaging.py | 302 +++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 SoftLayer/CLI/modules/messaging.py diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py new file mode 100644 index 000000000..1457b1c0a --- /dev/null +++ b/SoftLayer/CLI/modules/messaging.py @@ -0,0 +1,302 @@ +""" +usage: sl messaging [] [...] [options] + +Manage SoftLayer Message Queue + +The available commands are: + account List all queue accounts + queue Queue-related commands + topic Topic-related commands +""" +# :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. +# :license: BSD, see LICENSE for more details. +# from SoftLayer import NetworkManager +import sys + +from SoftLayer.CLI import CLIRunnable, Table +from SoftLayer.CLI.helpers import CLIAbort, listing, ArgumentError + +try: + import softlayer_messaging +except ImportError: + raise CLIAbort("""This functionality requires the softlayer_messaging package. +Run 'pip install softlayer_messaging' to install.""") + + +def get_mq_client(account_id, env): + client = softlayer_messaging.get_client(account_id) + client.authenticate(env.config.get('username'), env.config.get('api_key')) + return client + + +class ListAccounts(CLIRunnable): + """ +usage: sl messaging list-accounts [options] + +List SoftLayer Message Queue Accounts + +""" + action = 'list-accounts' + + @staticmethod + def execute(client, args): + accounts = client['Account'].getMessageQueueAccounts( + mask='id,name,status,nodes') + + t = Table([ + 'id', 'name', 'status' + ]) + for account in accounts: + t.add_row([ + account['nodes'][0]['accountName'], + account['name'], + account['status']['name'], + ]) + + return t + + +def queue_table(queue): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['name', queue['name']]) + t.add_row(['message_count', queue['message_count']]) + t.add_row(['visible_message_count', queue['visible_message_count']]) + t.add_row(['tags', listing(queue['tags'] or [])]) + t.add_row(['expiration', queue['expiration']]) + t.add_row(['visibility_interval', queue['visibility_interval']]) + return t + + +def message_table(message): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['id', message['id']]) + t.add_row(['initial_entry_time', message['initial_entry_time']]) + t.add_row(['visibility_delay', message['visibility_delay']]) + t.add_row(['visibility_interval', message['visibility_interval']]) + t.add_row(['fields', message['fields']]) + return [t, message['body']] + + +def topic_table(topic): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['name', topic['name']]) + t.add_row(['tags', listing(topic['tags'] or [])]) + return t + + +def subscription_table(sub): + t = Table(['property', 'value']) + t.align['property'] = 'r' + t.align['value'] = 'l' + + t.add_row(['id', sub['id']]) + t.add_row(['endpoint_type', sub['endpoint_type']]) + for k, v in sub['endpoint'].items(): + t.add_row([k, v]) + return t + + +class Queue(CLIRunnable): + """ +usage: sl messaging queue list [options] + sl messaging queue detail [options] + sl messaging queue create [options] + sl messaging queue modify [options] + sl messaging queue delete [] [options] + sl messaging queue push ( | [-]) [options] + sl messaging queue pop [options] + +Manage queues + +Queue Create/modify Options: + --visibility_interval=SECONDS Time in seconds that messages will re-appear + after being popped + --expiration=SECONDS Time in seconds that messages will live + --tags=TAGS Comma-separated list of tags + +Queue Delete Options: + --force Flag to force the deletion of the queue even when there are messages + +Pop Options: + --count=NUM Count of messages to pop +""" + action = 'queue' + + @classmethod + def execute(cls, client, args): + mq_client = get_mq_client(args[''], cls.env) + + # list + if args['list']: + queues = mq_client.queues()['items'] + + t = Table([ + 'name', 'message_count', 'visible_message_count' + ]) + for queue in queues: + t.add_row([ + queue['name'], + queue['message_count'], + queue['visible_message_count'], + ]) + return t + # detail + elif args['detail']: + queue = mq_client.queue(args['']).detail() + return queue_table(queue) + # create + elif args['create'] or args['modify']: + tags = None + if args.get('--tags'): + tags = [tag.strip() for tag in args.get('--tags').split(',')] + + queue = mq_client.create_queue( + args[''], + visibility_interval=int(args.get('--visibility_interval') or 30), + expiration=int(args.get('--expiration') or 604800), + tags=tags, + ) + return queue_table(queue) + # delete + elif args['delete']: + if args['']: + messages = mq_client.queue(args['']).message( + args['']).delete() + else: + mq_client.queue(args['']).delete( + args.get('--force')) + # push message + elif args['push']: + # the message body comes from the positional argument or stdin + body = '' + if args[''] is not None: + body = args[''] + else: + body = sys.stdin.read() + return message_table( + mq_client.queue(args['']).push(body)) + # pop message + elif args['pop']: + messages = mq_client.queue(args['']).pop( + args.get('--count') or 1) + formatted_messages = [] + for message in messages['items']: + formatted_messages.append(message_table(message)) + return formatted_messages + else: + raise CLIAbort('Invalid command') + + +class Topic(CLIRunnable): + """ +usage: sl messaging topic list [options] + sl messaging topic detail [options] + sl messaging topic create [options] + sl messaging topic delete [] [options] + sl messaging topic push ( | [-]) [options] + +Manage topics and subscriptions + +Topic/Subscription Create Options: + --subscription Create a subscription + --type=TYPE Type of endpoint, [Options: http, queue] + --queue-name=NAME Queue name. Required if --type is queue + --http-method=METHOD HTTP Method to use if --type is http + --http-url=URL HTTP/HTTPS URL to use. Required if --type is http + --http-body=BODY HTTP Body template to use if --type is http + +Topic Delete Options: + --force Flag to force the deletion of the topic even when there are subscriptions + +""" + action = 'topic' + + @classmethod + def execute(cls, client, args): + mq_client = get_mq_client(args[''], cls.env) + + # list + if args['list']: + topics = mq_client.topics()['items'] + + t = Table(['name']) + for topic in topics: + t.add_row([topic['name']]) + return t + # detail + elif args['detail']: + topic = mq_client.topic(args['']).detail() + subscriptions = mq_client.topic(args['']).subscriptions() + tables = [] + for sub in subscriptions['items']: + tables.append(subscription_table(sub)) + return [topic_table(topic), tables] + # create + elif args['create']: + if args['--subscription']: + topic = mq_client.topic(args['']) + if args['--type'] == 'queue': + subscription = topic.create_subscription( + 'queue', + queue_name=args['--queue-name'], + ) + elif args['--type'] == 'http': + subscription = topic.create_subscription( + 'http', + method=args['--http-method'] or 'GET', + url=args['--http-url'], + body=args['--http-body'] + ) + else: + raise ArgumentError( + '--type should be either queue or http.') + return subscription_table(subscription) + else: + tags = None + if args.get('--tags'): + tags = [tag.strip() for tag in args.get('--tags').split(',')] + + topic = mq_client.create_topic( + args[''], + visibility_interval=int(args.get('--visibility_interval') or 30), + expiration=int(args.get('--expiration') or 604800), + tags=tags, + ) + return topic_table(topic) + # delete + elif args['delete']: + if args['']: + messages = mq_client.topic(args['']).subscription( + args['']).delete() + else: + mq_client.topic(args['']).delete( + args.get('--force')) + # push message + elif args['push']: + # the message body comes from the positional argument or stdin + body = '' + if args[''] is not None: + body = args[''] + else: + body = sys.stdin.read() + return message_table( + mq_client.topic(args['']).push(body)) + # pop message + elif args['pop']: + messages = mq_client.topic(args['']).pop( + args.get('--count') or 1) + formatted_messages = [] + for message in messages['items']: + formatted_messages.append(message_table(message)) + return formatted_messages + else: + raise CLIAbort('Invalid command') From dd804ae7076860b8ad8eceac3f1df652ae38cb5b Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 30 May 2013 15:41:52 -0500 Subject: [PATCH 0042/3227] Corrects reference to list-accounts --- SoftLayer/CLI/modules/messaging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py index 1457b1c0a..76c23fce4 100644 --- a/SoftLayer/CLI/modules/messaging.py +++ b/SoftLayer/CLI/modules/messaging.py @@ -4,9 +4,9 @@ Manage SoftLayer Message Queue The available commands are: - account List all queue accounts - queue Queue-related commands - topic Topic-related commands + list-accounts List all queue accounts + queue Queue-related commands + topic Topic-related commands """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. From 6810e9783f40a3b5e60e4dba492ece10bc8bcf2b Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Tue, 4 Jun 2013 11:03:18 -0500 Subject: [PATCH 0043/3227] Doc: URL fixups --- docs/cli.rst | 6 +++--- docs/install.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index bcfa9727a..37eca772f 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -47,13 +47,13 @@ The only required fields are `username` and `api_key`. You can optionally also/e [softlayer] username = username api_key = oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha - endpoint_url = http://api.softlayer.com/xmlrpc/v3/ + endpoint_url = https://api.softlayer.com/xmlrpc/v3/ *exclusive url* :: [softlayer] - endpoint_url = http://api.softlayer.com/xmlrpc/v3/ + endpoint_url = https://api.softlayer.com/xmlrpc/v3/ Usage Examples @@ -154,4 +154,4 @@ Most commands will take in additional options/arguments. To see all available ac Standard Options: --format=ARG Output format. [Options: table, raw] [Default: table] -C FILE --config=FILE Config file location. [Default: ~/.softlayer] - -h --help Show this screen \ No newline at end of file + -h --help Show this screen diff --git a/docs/install.rst b/docs/install.rst index cc6cd6d3a..41d8f26e0 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -14,7 +14,7 @@ Install from source gia pip (requires git): :: $ pip install git+git://github.com/softlayer/softlayer-api-python-client.git -The most up to date version of this library can be found on the SoftLayer GitHub public repositories: http://github.com/softlayer. Please post to the SoftLayer forums http://forums.softlayer.com/ or open a support ticket in the SoftLayer customer portal if you have any questions regarding use of this library. +The most up to date version of this library can be found on the SoftLayer GitHub public repositories: https://github.com/softlayer. Please post to the SoftLayer forums https://forums.softlayer.com/ or open a support ticket in the SoftLayer customer portal if you have any questions regarding use of this library. From Source @@ -39,4 +39,4 @@ Or, download the `zipball Date: Tue, 4 Jun 2013 11:42:57 -0500 Subject: [PATCH 0044/3227] dns: fixed delete zone in CLI --- SoftLayer/CLI/modules/dns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 33b703e77..527852c53 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -73,7 +73,8 @@ class DeleteZone(CLIRunnable): def execute(client, args): manager = DNSManager(client) if args['--really'] or no_going_back(args['']): - manager.delete_zone(args['']) + zone_id = manager.get_zone(args['']) + manager.delete_zone(zone_id) raise CLIAbort("Aborted.") From daf15ce22f76ce7d2efd3d523d77b49c2f4ab198 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Tue, 4 Jun 2013 14:19:04 -0500 Subject: [PATCH 0045/3227] List and filter DNS records CLI: added optional zone name to the dns list command CLI: Added optional filters to the dns list command DNS: added get_records() call. --- SoftLayer/CLI/modules/dns.py | 55 +++++++++++++++++++++++++++++++++--- SoftLayer/managers/dns.py | 40 ++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index 527852c53..a9d8dc288 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -7,7 +7,7 @@ search Look for a resource record by exact name edit Update resource records (bulk/single) create Create zone - list List zones + list List zones or a zone's records remove Remove resource records add Add resource record print Print zone in BIND format @@ -80,14 +80,61 @@ def execute(client, args): class ListZones(CLIRunnable): """ -usage: sl dns list [options] +usage: sl dns list [] [options] -List zones +List zones and optionally, records + +Filters: + --type=TYPE Record type, such as A or CNAME + --data=DATA Record data, such as an IP address + --record=HOST Host record, such as www + --ttl=TTL TTL value in seconds, such as 86400 """ action = 'list' + @classmethod + def execute(cls, client, args): + if args['']: + return cls.list_zone(client, args[''], args) + + return cls.list_zones() + @staticmethod - def execute(client, args): + def list_zone(client, zone, args): + manager = DNSManager(client) + t = Table([ + "record", + "type", + "ttl", + "value", + ]) + + t.align['ttl'] = 'l' + t.align['record'] = 'r' + t.align['value'] = 'l' + + try: + records = manager.get_records(args[''], + type=args.get('--type'), + host=args.get('--record'), + ttl=args.get('--ttl'), + data=args.get('--data'), + ) + except DNSZoneNotFound: + raise CLIAbort("No zone found matching: %s" % args['']) + + for rr in records: + t.add_row([ + rr['host'], + rr['type'].upper(), + rr['ttl'], + rr['data'] + ]) + + return t + + @staticmethod + def list_zones(client): manager = DNSManager(client) zones = manager.list_zones() t = Table([ diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index eb70fbad1..ee4d7db22 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -7,6 +7,7 @@ :license: BSD, see LICENSE for more details. """ from time import strftime + from SoftLayer.exceptions import DNSZoneNotFound @@ -116,6 +117,45 @@ def search_record(self, zone, record): records = filter(lambda x: x['host'].lower() == record.lower(), rrs) return records + def get_records(self, zone, ttl=None, data=None, host=None, + type=None, **kwargs): + """ List, and optionally filter, records within a zone. + + :param zone: the zone name in which to search. + :param int ttl: optionally, time in seconds: + :param data: optionally, the records data + :param host: optionally, record's host + :param type: optionally, the type of record: + + :returns iterator: + """ + check = [] + + if ttl: + check.append(lambda x: x['ttl'] == ttl) + + if host: + check.append(lambda x: x['record'] == host) + + if data: + check.append(lambda x: x['data'] == data) + + if type: + check.append(lambda x: x['type'] == type.lower()) + + try: + results = self.service.getByDomainName( + zone, + mask='resourceRecords', + )[0]['resourceRecords'] + except IndexError: + raise DNSZoneNotFound(zone) + + # Make sure all requested filters are truthful + filter_results = lambda x: all(v(x) for v in check) + + return filter(filter_results, results) + def edit_record(self, record): """ Update an existing record with the options provided. The provided dict must include an 'id' key and value corresponding to the record From 71b5d1a1d5eb009ce66e5f855175247c7d89d519 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Tue, 4 Jun 2013 15:57:46 -0500 Subject: [PATCH 0046/3227] DNS: tests get_records --- SoftLayer/managers/dns.py | 2 +- SoftLayer/tests/managers/dns_tests.py | 77 +++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index ee4d7db22..5af404d0b 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -148,7 +148,7 @@ def get_records(self, zone, ttl=None, data=None, host=None, zone, mask='resourceRecords', )[0]['resourceRecords'] - except IndexError: + except (IndexError, KeyError, TypeError): raise DNSZoneNotFound(zone) # Make sure all requested filters are truthful diff --git a/SoftLayer/tests/managers/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py index c80c19161..4eff55f9a 100644 --- a/SoftLayer/tests/managers/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -113,3 +113,80 @@ def test_dump_zone(self): f.return_value = 'lots of text' self.dns_client.dump_zone(1) f.assert_called_once_with(id=1) + + def test_get_records_bad_results(self): + f = self.client['Dns_Domain'].getByDomainName + f.return_value = None + + # handle all of the bad return values from the API + self.assertRaises(DNSZoneNotFound, + self.dns_client.get_records, 'non-existent') + + f.return_value = [] + self.assertRaises(DNSZoneNotFound, + self.dns_client.get_records, 'non-existent') + + f.return_value = [{}] + self.assertRaises(DNSZoneNotFound, + self.dns_client.get_records, 'non-existent') + + def test_get_records_good_results(self): + f = self.client['Dns_Domain'].getByDomainName + f.return_value = [{'resourceRecords': [ + {'ttl': 7200, 'data': 'd', 'record': 'a', 'type': 'cname'}, + {'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}, + {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}, + {'ttl': 86400, 'data': 'b', 'record': 'd', 'type': 'txt'}, + {'ttl': 86400, 'data': 'b', 'record': 'e', 'type': 'txt'}, + {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, + ]}] + + # simple 1/4 matches + results = self.dns_client.get_records('z', host='b') + self.assertEqual(results, + [{'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}]) + + results = self.dns_client.get_records('z', type='ptr') + self.assertEqual(results, + [{'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}]) + + results = self.dns_client.get_records('z', ttl=900) + self.assertEqual(results, [ + {'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}, + {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}]) + + results = self.dns_client.get_records('z', data='x') + self.assertEqual(results, [ + {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'} + ]) + + def test_get_records_strict_results(self): + f = self.client['Dns_Domain'].getByDomainName + f.return_value = [{'resourceRecords': [ + {'ttl': 7200, 'data': 'd', 'record': 'a', 'type': 'cname'}, + {'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}, + {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}, + {'ttl': 86400, 'data': 'b', 'record': 'd', 'type': 'txt'}, + {'ttl': 86400, 'data': 'b', 'record': 'e', 'type': 'txt'}, + {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, + ]}] + + # 2/4 match + results = self.dns_client.get_records('z', data='b', type='txt') + self.assertEqual(results, [ + {'ttl': 86400, 'data': 'b', 'record': 'd', 'type': 'txt'}, + {'ttl': 86400, 'data': 'b', 'record': 'e', 'type': 'txt'}, + {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, + ]) + + # 3/4 match + results = self.dns_client.get_records('z', + data='b', type='txt', ttl=600) + self.assertEqual(results, [ + {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, + ]) + + # 2/4 match, 1 non-matching + results = self.dns_client.get_records('z', + data='1', type='a', ttl=600) + self.assertEqual(results, []) From 4d6ccb1cea7eb8184976db528fca2b1ccccea4d6 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Wed, 5 Jun 2013 09:20:02 -0500 Subject: [PATCH 0047/3227] Fixed sl dns list --- SoftLayer/CLI/modules/dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index a9d8dc288..b36a79269 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -97,7 +97,7 @@ def execute(cls, client, args): if args['']: return cls.list_zone(client, args[''], args) - return cls.list_zones() + return cls.list_zones(client) @staticmethod def list_zone(client, zone, args): From a3dd808c57592d55f8261c7d88c66e42d5b7eb63 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 5 Jun 2013 15:21:21 -0500 Subject: [PATCH 0048/3227] Fixing iSCSI documentation --- SoftLayer/CLI/modules/iscsi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/iscsi.py b/SoftLayer/CLI/modules/iscsi.py index 79aeffc88..3401705c6 100644 --- a/SoftLayer/CLI/modules/iscsi.py +++ b/SoftLayer/CLI/modules/iscsi.py @@ -4,7 +4,7 @@ Manage iSCSI targets The available commands are: - list List NAS accounts + list List iSCSI targets """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. From c9304410de06070405f87d8558a2786aed843690 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 7 Jun 2013 11:36:22 -0500 Subject: [PATCH 0049/3227] Adds CPU and memory filters for hardware NOTE: This is pending an API version release. --- SoftLayer/CLI/modules/hardware.py | 4 ++++ SoftLayer/managers/hardware.py | 14 +++++++++++--- SoftLayer/tests/managers/hardware_tests.py | 4 ++++ SoftLayer/utils.py | 5 +++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 1d040a3a3..6224faffb 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -50,6 +50,8 @@ class ListHardware(CLIRunnable): Filters: -H --hostname=HOST Host portion of the FQDN. example: server -D --domain=DOMAIN Domain portion of the FQDN. example: example.com + -c --cpu=CPU Number of CPU cores + -m --memory=MEMORY Memory in gigabytes -d DC, --datacenter=DC datacenter shortname (sng01, dal05, ...) -n MBPS, --network=MBPS Network port speed in Mbps --tags=ARG Only show instances that have one of these tags. @@ -70,6 +72,8 @@ def execute(client, args): servers = manager.list_hardware( hostname=args.get('--hostname'), domain=args.get('--domain'), + cpus=args.get('--cpu'), + memory=args.get('--memory'), datacenter=args.get('--datacenter'), nic_speed=args.get('--network'), tags=tags) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index e350da5a6..12b30b18f 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -25,12 +25,14 @@ def __init__(self, client): self.account = self.client['Account'] self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] - def list_hardware(self, tags=None, hostname=None, domain=None, - datacenter=None, nic_speed=None, public_ip=None, - private_ip=None, **kwargs): + 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. :param list tags: filter based on tags + :param integer cpus: filter based on number of CPUS + :param integer memory: filter based on amount of memory in gigabytes :param string hostname: filter based on hostname :param string domain: filter based on domain :param string datacenter: filter based on datacenter @@ -61,6 +63,12 @@ def list_hardware(self, tags=None, hostname=None, domain=None, 'options': [{'name': 'data', 'value': tags}], } + if cpus: + _filter['hardware']['processorCoreAmount'] = query_filter(cpus) + + if memory: + _filter['hardware']['memoryCapacity'] = query_filter(memory) + if hostname: _filter['hardware']['hostname'] = query_filter(hostname) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 13aa2ec01..f01b8d307 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -30,6 +30,8 @@ def test_list_hardware(self): def test_list_hardware_with_filters(self): self.hardware.list_hardware( tags=['tag1', 'tag2'], + cpus=2, + memory=1, hostname='hostname', domain='example.com', datacenter='dal05', @@ -50,6 +52,8 @@ def test_list_hardware_with_filters(self): {'name': 'data', 'value': ['tag1', 'tag2']}] }} }, + 'memoryCapacity': {'operation': 1}, + 'processorCoreAmount': {'operation': 2}, 'hostname': {'operation': '_= hostname'}, 'primaryIpAddress': {'operation': '_= 1.2.3.4'}, 'networkComponents': {'maxSpeed': {'operation': 100}}, diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 935a397c5..4984622b8 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -47,6 +47,11 @@ def query_filter(query): :param string query: query string """ + try: + query = int(query) + except ValueError: + pass + if isinstance(query, basestring): query = query.strip() for op in KNOWN_OPERATIONS: From 2706e6f256caea2604a246add49fd155c79db360 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 10 Jun 2013 13:30:45 -0500 Subject: [PATCH 0050/3227] Add in network components to the get_hardware() call --- SoftLayer/managers/hardware.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index e350da5a6..f17b29238 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -108,6 +108,8 @@ def get_hardware(self, id, **kwargs): 'primaryBackendIpAddress', 'primaryIpAddress', 'datacenter.name', + 'networkComponents[status, maxSpeed, name, ipmiMacAddress,' \ + 'ipmiIpAddress]', 'activeTransaction.id', 'operatingSystem.softwareLicense.' 'softwareDescription[manufacturer,name,version,referenceCode]', From 1c093ea22299c3552fc27f1849b36f01f6d9b6f6 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 10 Jun 2013 13:52:04 -0500 Subject: [PATCH 0051/3227] Adding some missing properties to the mask --- SoftLayer/managers/hardware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index f17b29238..91d0829e7 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -109,7 +109,7 @@ def get_hardware(self, id, **kwargs): 'primaryIpAddress', 'datacenter.name', 'networkComponents[status, maxSpeed, name, ipmiMacAddress,' \ - 'ipmiIpAddress]', + 'ipmiIpAddress, macAddress, primaryIpAddress]', 'activeTransaction.id', 'operatingSystem.softwareLicense.' 'softwareDescription[manufacturer,name,version,referenceCode]', From 12911e7e1617349ab079419857f1b2de25c4d4a1 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 10 Jun 2013 13:57:41 -0500 Subject: [PATCH 0052/3227] Adding another network property --- SoftLayer/managers/hardware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 91d0829e7..38926efe3 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -109,7 +109,7 @@ def get_hardware(self, id, **kwargs): 'primaryIpAddress', 'datacenter.name', 'networkComponents[status, maxSpeed, name, ipmiMacAddress,' \ - 'ipmiIpAddress, macAddress, primaryIpAddress]', + 'ipmiIpAddress, macAddress, primaryIpAddress, port]', 'activeTransaction.id', 'operatingSystem.softwareLicense.' 'softwareDescription[manufacturer,name,version,referenceCode]', From cae5af5d9cb59837df31ee11b26d8a232670ff81 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 10 Jun 2013 14:13:47 -0500 Subject: [PATCH 0053/3227] Adding in primary subnet information --- SoftLayer/managers/hardware.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 38926efe3..ae06e5ea7 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -109,7 +109,10 @@ def get_hardware(self, id, **kwargs): 'primaryIpAddress', 'datacenter.name', 'networkComponents[status, maxSpeed, name, ipmiMacAddress,' \ - 'ipmiIpAddress, macAddress, primaryIpAddress, port]', + 'ipmiIpAddress, macAddress, primaryIpAddress, port,' \ + 'primarySubnet]', + 'networkComponents.primarySubnet[netmask, broadcastAddress,' \ + 'networkIdentifier, gateway]', 'activeTransaction.id', 'operatingSystem.softwareLicense.' 'softwareDescription[manufacturer,name,version,referenceCode]', From c306f7e0e9636b30f80cff691b077b408617bf17 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 10 Jun 2013 14:34:10 -0500 Subject: [PATCH 0054/3227] Adding ID parameters for future use --- SoftLayer/managers/hardware.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index ae06e5ea7..8dcfec251 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -108,11 +108,11 @@ def get_hardware(self, id, **kwargs): 'primaryBackendIpAddress', 'primaryIpAddress', 'datacenter.name', - 'networkComponents[status, maxSpeed, name, ipmiMacAddress,' \ - 'ipmiIpAddress, macAddress, primaryIpAddress, port,' \ - 'primarySubnet]', - 'networkComponents.primarySubnet[netmask, broadcastAddress,' \ - 'networkIdentifier, gateway]', + 'networkComponents[id, status, maxSpeed, name,' \ + 'ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,'\ + 'port, primarySubnet]', + 'networkComponents.primarySubnet[id, netmask,' \ + 'broadcastAddress, networkIdentifier, gateway]', 'activeTransaction.id', 'operatingSystem.softwareLicense.' 'softwareDescription[manufacturer,name,version,referenceCode]', From a4d9874f5c3af569798621550a61c6f5c4d07fcd Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 10 Jun 2013 15:59:02 -0500 Subject: [PATCH 0055/3227] Adding name and domain options in addition to FQDN. Also attempting to fix a bug with the active transaction friendly name not always being present. --- SoftLayer/CLI/modules/cci.py | 3 +-- SoftLayer/managers/cci.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index b6503f4f4..3ca74ebb6 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -117,8 +117,7 @@ def execute(client, args): mb_to_gb(guest['maxMemory']), guest['primaryIpAddress'] or blank(), guest['primaryBackendIpAddress'] or blank(), - guest['activeTransaction']['transactionStatus'] - ['friendlyName'] or blank(), + guest['activeTransaction']['transactionStatus'].get('friendlyName', blank()), ]) return t diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 62ec47a59..5295c4550 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -46,6 +46,8 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, items = set([ 'id', 'globalIdentifier', + 'hostname', + 'domain', 'fullyQualifiedDomainName', 'primaryBackendIpAddress', 'primaryIpAddress', From 597fa49e0ea1033d5cfeb1189e3c0f5c895f80d2 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 10 Jun 2013 16:41:09 -0500 Subject: [PATCH 0056/3227] Changing statement to 'or blank()' per recommendation --- SoftLayer/CLI/modules/cci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 3ca74ebb6..5de8f08ce 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -117,7 +117,7 @@ def execute(client, args): mb_to_gb(guest['maxMemory']), guest['primaryIpAddress'] or blank(), guest['primaryBackendIpAddress'] or blank(), - guest['activeTransaction']['transactionStatus'].get('friendlyName', blank()), + guest['activeTransaction']['transactionStatus'].get('friendlyName') or blank(), ]) return t From e21ff72e32b8b67bb3758b12ce78c141c107d215 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 11 Jun 2013 08:30:27 -0500 Subject: [PATCH 0057/3227] Add domain to the mask as well --- SoftLayer/managers/hardware.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 8dcfec251..71edd35ed 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -44,6 +44,7 @@ def list_hardware(self, tags=None, hostname=None, domain=None, items = set([ 'id', 'hostname', + 'domain', 'globalIdentifier', 'fullyQualifiedDomainName', 'processorCoreAmount', From ea1ccf46c4b3c78eb32441b2fad6cf5e954b67d5 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 11 Jun 2013 11:07:23 -0500 Subject: [PATCH 0058/3227] Fixed an issue with the host name (record) filter caused by an invalid key name. Also expanded the filter so that it could support asterisk wildcards. --- SoftLayer/managers/dns.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index 5af404d0b..c0f30ec9f 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -6,6 +6,7 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ +import re from time import strftime from SoftLayer.exceptions import DNSZoneNotFound @@ -24,6 +25,7 @@ def __init__(self, client): self.service = self.client['Dns_Domain'] self.record = self.client['Dns_Domain_ResourceRecord'] + def list_zones(self, **kwargs): """ Retrieve a list of all DNS zones. @@ -32,6 +34,7 @@ def list_zones(self, **kwargs): """ return self.client['Account'].getDomains(**kwargs) + def get_zone(self, zone): """ Get a zone and its records. @@ -49,6 +52,7 @@ def get_zone(self, zone): except IndexError: raise DNSZoneNotFound(zone) + def create_zone(self, zone, serial=None): """ Create a zone for the specified zone. @@ -61,6 +65,7 @@ def create_zone(self, zone, serial=None): 'serial': serial or strftime('%Y%m%d01'), "resourceRecords": {}}) + def delete_zone(self, id): """ Delete a zone by its ID. @@ -69,6 +74,7 @@ def delete_zone(self, id): """ return self.service.deleteObject(id=id) + def edit_zone(self, zone): """ Update an existing zone with the options provided. The provided dict must include an 'id' key and value corresponding to the zone that @@ -79,6 +85,7 @@ def edit_zone(self, zone): """ self.service.editObject(zone) + def create_record(self, id, record, type, data, ttl=60): """ Create a resource record on a domain. @@ -96,6 +103,7 @@ def create_record(self, id, record, type, data, ttl=60): 'type': type, 'data': data}) + def delete_record(self, recordid): """ Delete a resource record by its ID. @@ -104,6 +112,7 @@ def delete_record(self, recordid): """ self.record.deleteObject(id=recordid) + def search_record(self, zone, record): """ Search for records on a zone that match a specific name. Useful for validating whether a record exists or that it has the @@ -117,6 +126,7 @@ def search_record(self, zone, record): records = filter(lambda x: x['host'].lower() == record.lower(), rrs) return records + def get_records(self, zone, ttl=None, data=None, host=None, type=None, **kwargs): """ List, and optionally filter, records within a zone. @@ -135,7 +145,8 @@ def get_records(self, zone, ttl=None, data=None, host=None, check.append(lambda x: x['ttl'] == ttl) if host: - check.append(lambda x: x['record'] == host) + check.append(lambda x: re.search(self._translate_filter(host), + x['host'])) if data: check.append(lambda x: x['data'] == data) @@ -156,6 +167,7 @@ def get_records(self, zone, ttl=None, data=None, host=None, return filter(filter_results, results) + def edit_record(self, record): """ Update an existing record with the options provided. The provided dict must include an 'id' key and value corresponding to the record @@ -166,6 +178,7 @@ def edit_record(self, record): """ self.record.editObject(record, id=record['id']) + def dump_zone(self, id): """ Retrieve a zone dump in BIND format. @@ -173,3 +186,24 @@ def dump_zone(self, id): """ return self.service.getZoneFileContents(id=id) + + + def _translate_filter(self, query): + """ This function takes command line query syntax and changes it into + regular expressions. + + This is a temporary workaround until the API supports zone filtering. + + :param string query: The query string to translate. + """ + if isinstance(query, basestring): + if query.startswith('*') and query.endswith('*'): + query = "^.*%s.*$" % query.strip('*') + elif query.startswith('*'): + query = "^.*%s$" % query.strip('*') + elif query.endswith('*'): + query = "^%s.*$" % query.strip('*') + else: + query = "^%s$" % query.strip('*') + + return query \ No newline at end of file From d12c9d038655e6ce0a8f78b2898238ca702b1129 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 11 Jun 2013 11:18:15 -0500 Subject: [PATCH 0059/3227] Fixing DNS unit tests. The mock dictionary that was being created did not properly represent the data being returned by the API. I suspect this was the cause of the invalid key error that this branch was created to fix. --- SoftLayer/tests/managers/dns_tests.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/tests/managers/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py index 4eff55f9a..b3d51d07a 100644 --- a/SoftLayer/tests/managers/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -133,31 +133,31 @@ def test_get_records_bad_results(self): def test_get_records_good_results(self): f = self.client['Dns_Domain'].getByDomainName f.return_value = [{'resourceRecords': [ - {'ttl': 7200, 'data': 'd', 'record': 'a', 'type': 'cname'}, - {'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}, - {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}, - {'ttl': 86400, 'data': 'b', 'record': 'd', 'type': 'txt'}, - {'ttl': 86400, 'data': 'b', 'record': 'e', 'type': 'txt'}, - {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, + {'ttl': 7200, 'data': 'd', 'host': 'a', 'type': 'cname'}, + {'ttl': 900, 'data': '1', 'host': 'b', 'type': 'a'}, + {'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'}, + {'ttl': 86400, 'data': 'b', 'host': 'd', 'type': 'txt'}, + {'ttl': 86400, 'data': 'b', 'host': 'e', 'type': 'txt'}, + {'ttl': 600, 'data': 'b', 'host': 'f', 'type': 'txt'}, ]}] # simple 1/4 matches results = self.dns_client.get_records('z', host='b') self.assertEqual(results, - [{'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}]) + [{'ttl': 900, 'data': '1', 'host': 'b', 'type': 'a'}]) results = self.dns_client.get_records('z', type='ptr') self.assertEqual(results, - [{'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}]) + [{'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'}]) results = self.dns_client.get_records('z', ttl=900) self.assertEqual(results, [ - {'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}, - {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}]) + {'ttl': 900, 'data': '1', 'host': 'b', 'type': 'a'}, + {'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'}]) results = self.dns_client.get_records('z', data='x') self.assertEqual(results, [ - {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'} + {'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'} ]) def test_get_records_strict_results(self): From 8a82e6cdd4a134272336c455e0bb434465b2e03a Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 11 Jun 2013 11:25:53 -0500 Subject: [PATCH 0060/3227] Removing excess whitespace caused by Emacs formatting --- SoftLayer/managers/dns.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index c0f30ec9f..dddf2900a 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -24,7 +24,6 @@ def __init__(self, client): self.client = client self.service = self.client['Dns_Domain'] self.record = self.client['Dns_Domain_ResourceRecord'] - def list_zones(self, **kwargs): """ Retrieve a list of all DNS zones. @@ -33,7 +32,6 @@ def list_zones(self, **kwargs): """ return self.client['Account'].getDomains(**kwargs) - def get_zone(self, zone): """ Get a zone and its records. @@ -51,7 +49,6 @@ def get_zone(self, zone): return matches[0] except IndexError: raise DNSZoneNotFound(zone) - def create_zone(self, zone, serial=None): """ Create a zone for the specified zone. @@ -64,7 +61,6 @@ def create_zone(self, zone, serial=None): 'name': zone, 'serial': serial or strftime('%Y%m%d01'), "resourceRecords": {}}) - def delete_zone(self, id): """ Delete a zone by its ID. @@ -74,7 +70,6 @@ def delete_zone(self, id): """ return self.service.deleteObject(id=id) - def edit_zone(self, zone): """ Update an existing zone with the options provided. The provided dict must include an 'id' key and value corresponding to the zone that @@ -84,7 +79,6 @@ def edit_zone(self, zone): """ self.service.editObject(zone) - def create_record(self, id, record, type, data, ttl=60): """ Create a resource record on a domain. @@ -102,7 +96,6 @@ def create_record(self, id, record, type, data, ttl=60): 'host': record, 'type': type, 'data': data}) - def delete_record(self, recordid): """ Delete a resource record by its ID. @@ -111,7 +104,6 @@ def delete_record(self, recordid): """ self.record.deleteObject(id=recordid) - def search_record(self, zone, record): """ Search for records on a zone that match a specific name. @@ -125,7 +117,6 @@ def search_record(self, zone, record): rrs = self.get_zone(zone)['resourceRecords'] records = filter(lambda x: x['host'].lower() == record.lower(), rrs) return records - def get_records(self, zone, ttl=None, data=None, host=None, type=None, **kwargs): @@ -166,7 +157,6 @@ def get_records(self, zone, ttl=None, data=None, host=None, filter_results = lambda x: all(v(x) for v in check) return filter(filter_results, results) - def edit_record(self, record): """ Update an existing record with the options provided. The provided @@ -177,7 +167,6 @@ def edit_record(self, record): """ self.record.editObject(record, id=record['id']) - def dump_zone(self, id): """ Retrieve a zone dump in BIND format. @@ -186,7 +175,6 @@ def dump_zone(self, id): """ return self.service.getZoneFileContents(id=id) - def _translate_filter(self, query): """ This function takes command line query syntax and changes it into From bbb6dae6cb3eb8feb98a88f551d75668f3928220 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 11 Jun 2013 11:31:50 -0500 Subject: [PATCH 0061/3227] Removing extraneous whitespace --- SoftLayer/managers/dns.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index dddf2900a..9b1e149e2 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -24,7 +24,7 @@ def __init__(self, client): self.client = client self.service = self.client['Dns_Domain'] self.record = self.client['Dns_Domain_ResourceRecord'] - + def list_zones(self, **kwargs): """ Retrieve a list of all DNS zones. @@ -32,7 +32,7 @@ def list_zones(self, **kwargs): """ return self.client['Account'].getDomains(**kwargs) - + def get_zone(self, zone): """ Get a zone and its records. @@ -49,7 +49,7 @@ def get_zone(self, zone): return matches[0] except IndexError: raise DNSZoneNotFound(zone) - + def create_zone(self, zone, serial=None): """ Create a zone for the specified zone. @@ -61,7 +61,7 @@ def create_zone(self, zone, serial=None): 'name': zone, 'serial': serial or strftime('%Y%m%d01'), "resourceRecords": {}}) - + def delete_zone(self, id): """ Delete a zone by its ID. @@ -79,7 +79,7 @@ def edit_zone(self, zone): """ self.service.editObject(zone) - + def create_record(self, id, record, type, data, ttl=60): """ Create a resource record on a domain. @@ -96,7 +96,7 @@ def create_record(self, id, record, type, data, ttl=60): 'host': record, 'type': type, 'data': data}) - + def delete_record(self, recordid): """ Delete a resource record by its ID. @@ -104,7 +104,7 @@ def delete_record(self, recordid): """ self.record.deleteObject(id=recordid) - + def search_record(self, zone, record): """ Search for records on a zone that match a specific name. Useful for validating whether a record exists or that it has the @@ -117,7 +117,7 @@ def search_record(self, zone, record): rrs = self.get_zone(zone)['resourceRecords'] records = filter(lambda x: x['host'].lower() == record.lower(), rrs) return records - + def get_records(self, zone, ttl=None, data=None, host=None, type=None, **kwargs): """ List, and optionally filter, records within a zone. @@ -157,7 +157,7 @@ def get_records(self, zone, ttl=None, data=None, host=None, filter_results = lambda x: all(v(x) for v in check) return filter(filter_results, results) - + def edit_record(self, record): """ Update an existing record with the options provided. The provided dict must include an 'id' key and value corresponding to the record @@ -167,7 +167,7 @@ def edit_record(self, record): """ self.record.editObject(record, id=record['id']) - + def dump_zone(self, id): """ Retrieve a zone dump in BIND format. @@ -175,7 +175,7 @@ def dump_zone(self, id): """ return self.service.getZoneFileContents(id=id) - + def _translate_filter(self, query): """ This function takes command line query syntax and changes it into regular expressions. From 7ba7ffb2927018de6012704f337f6a8114eca6e9 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Thu, 13 Jun 2013 14:33:10 -0500 Subject: [PATCH 0062/3227] get_records() now does server side filtering --- SoftLayer/managers/dns.py | 69 +++++--------- SoftLayer/tests/managers/dns_tests.py | 126 ++++++++++++-------------- 2 files changed, 80 insertions(+), 115 deletions(-) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index 9b1e149e2..b536a3899 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -6,10 +6,10 @@ :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. :license: BSD, see LICENSE for more details. """ -import re from time import strftime from SoftLayer.exceptions import DNSZoneNotFound +from SoftLayer.utils import NestedDict, query_filter class DNSManager(object): @@ -33,20 +33,22 @@ def list_zones(self, **kwargs): """ return self.client['Account'].getDomains(**kwargs) - def get_zone(self, zone): + def get_zone(self, zone, records=True): """ Get a zone and its records. :param zone: the zone name """ zone = zone.lower() - results = self.service.getByDomainName( - zone, - mask={'resourceRecords': {}}) - matches = filter(lambda x: x['name'].lower() == zone, results) + mask = {} + if records: + mask = {'resourceRecords': {}} + results = self.client['Account'].getDomains( + filter={"domains": {"name": query_filter(zone)}}, + mask=mask) try: - return matches[0] + return results[0] except IndexError: raise DNSZoneNotFound(zone) @@ -128,35 +130,32 @@ def get_records(self, zone, ttl=None, data=None, host=None, :param host: optionally, record's host :param type: optionally, the type of record: - :returns iterator: + :returns list: """ - check = [] + _filter = NestedDict() if ttl: - check.append(lambda x: x['ttl'] == ttl) + _filter['resourceRecords']['ttl'] = query_filter(ttl) if host: - check.append(lambda x: re.search(self._translate_filter(host), - x['host'])) + _filter['resourceRecords']['host'] = query_filter(host) if data: - check.append(lambda x: x['data'] == data) + _filter['resourceRecords']['data'] = query_filter(data) if type: - check.append(lambda x: x['type'] == type.lower()) + _filter['resourceRecords']['type'] = \ + query_filter(type.lower()) - try: - results = self.service.getByDomainName( - zone, - mask='resourceRecords', - )[0]['resourceRecords'] - except (IndexError, KeyError, TypeError): - raise DNSZoneNotFound(zone) + domain = self.get_zone(zone, records=False) + results = self.service.getResourceRecords( + id=domain['id'], + mask='id,expire,domainId,host,minimum,refresh,retry,' + 'mxPriority,ttl,type,data,responsiblePerson', + filter=_filter.to_dict(), + ) - # Make sure all requested filters are truthful - filter_results = lambda x: all(v(x) for v in check) - - return filter(filter_results, results) + return results def edit_record(self, record): """ Update an existing record with the options provided. The provided @@ -175,23 +174,3 @@ def dump_zone(self, id): """ return self.service.getZoneFileContents(id=id) - - def _translate_filter(self, query): - """ This function takes command line query syntax and changes it into - regular expressions. - - This is a temporary workaround until the API supports zone filtering. - - :param string query: The query string to translate. - """ - if isinstance(query, basestring): - if query.startswith('*') and query.endswith('*'): - query = "^.*%s.*$" % query.strip('*') - elif query.startswith('*'): - query = "^.*%s$" % query.strip('*') - elif query.endswith('*'): - query = "^%s.*$" % query.strip('*') - else: - query = "^%s$" % query.strip('*') - - return query \ No newline at end of file diff --git a/SoftLayer/tests/managers/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py index b3d51d07a..373d1ae6d 100644 --- a/SoftLayer/tests/managers/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -37,11 +37,12 @@ def test_get_zone(self): ] # match - self.client['Dns_Domain'].getByDomainName.return_value = zone_list + self.client['Account'].getDomains.return_value = [zone_list[1]] res = self.dns_client.get_zone('example.com') self.assertEqual(res, zone_list[1]) # no match + self.client['Account'].getDomains.return_value = [] self.assertRaises( DNSZoneNotFound, self.dns_client.get_zone, @@ -88,7 +89,7 @@ def test_delete_record(self): def test_search_record(self): - self.client['Dns_Domain'].getByDomainName.return_value = [{ + self.client['Account'].getDomains.return_value = [{ 'name': 'example.com', 'resourceRecords': [ {'host': 'TEST1'}, @@ -114,79 +115,64 @@ def test_dump_zone(self): self.dns_client.dump_zone(1) f.assert_called_once_with(id=1) - def test_get_records_bad_results(self): - f = self.client['Dns_Domain'].getByDomainName - f.return_value = None - - # handle all of the bad return values from the API - self.assertRaises(DNSZoneNotFound, - self.dns_client.get_records, 'non-existent') - - f.return_value = [] - self.assertRaises(DNSZoneNotFound, - self.dns_client.get_records, 'non-existent') - - f.return_value = [{}] - self.assertRaises(DNSZoneNotFound, - self.dns_client.get_records, 'non-existent') - - def test_get_records_good_results(self): - f = self.client['Dns_Domain'].getByDomainName - f.return_value = [{'resourceRecords': [ + def test_get_record(self): + domain = {'id': 12345, 'name': 'example.com'} + records = [ {'ttl': 7200, 'data': 'd', 'host': 'a', 'type': 'cname'}, {'ttl': 900, 'data': '1', 'host': 'b', 'type': 'a'}, {'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'}, {'ttl': 86400, 'data': 'b', 'host': 'd', 'type': 'txt'}, {'ttl': 86400, 'data': 'b', 'host': 'e', 'type': 'txt'}, {'ttl': 600, 'data': 'b', 'host': 'f', 'type': 'txt'}, - ]}] - - # simple 1/4 matches - results = self.dns_client.get_records('z', host='b') - self.assertEqual(results, - [{'ttl': 900, 'data': '1', 'host': 'b', 'type': 'a'}]) + ] - results = self.dns_client.get_records('z', type='ptr') - self.assertEqual(results, - [{'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'}]) + A = self.client['Account'].getDomains + D = self.client['Dns_Domain'].getResourceRecords - results = self.dns_client.get_records('z', ttl=900) - self.assertEqual(results, [ - {'ttl': 900, 'data': '1', 'host': 'b', 'type': 'a'}, - {'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'}]) - - results = self.dns_client.get_records('z', data='x') - self.assertEqual(results, [ - {'ttl': 900, 'data': 'x', 'host': 'c', 'type': 'ptr'} - ]) - - def test_get_records_strict_results(self): - f = self.client['Dns_Domain'].getByDomainName - f.return_value = [{'resourceRecords': [ - {'ttl': 7200, 'data': 'd', 'record': 'a', 'type': 'cname'}, - {'ttl': 900, 'data': '1', 'record': 'b', 'type': 'a'}, - {'ttl': 900, 'data': 'x', 'record': 'c', 'type': 'ptr'}, - {'ttl': 86400, 'data': 'b', 'record': 'd', 'type': 'txt'}, - {'ttl': 86400, 'data': 'b', 'record': 'e', 'type': 'txt'}, - {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, - ]}] - - # 2/4 match - results = self.dns_client.get_records('z', data='b', type='txt') - self.assertEqual(results, [ - {'ttl': 86400, 'data': 'b', 'record': 'd', 'type': 'txt'}, - {'ttl': 86400, 'data': 'b', 'record': 'e', 'type': 'txt'}, - {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, - ]) - - # 3/4 match - results = self.dns_client.get_records('z', - data='b', type='txt', ttl=600) - self.assertEqual(results, [ - {'ttl': 600, 'data': 'b', 'record': 'f', 'type': 'txt'}, - ]) - - # 2/4 match, 1 non-matching - results = self.dns_client.get_records('z', - data='1', type='a', ttl=600) - self.assertEqual(results, []) + # Invalid domain + A.return_value = [] + self.assertRaises(DNSZoneNotFound, + self.dns_client.get_records, + 'example-not.com') + + # valid domain, but no records matching + A.return_value = [domain] + D.return_value = [] + self.assertEqual(self.dns_client.get_records('example.com'), + []) + + D.reset_mock() + A.return_value = [domain] + D.return_value = [records[1]] + self.dns_client.get_records('example.com', type='a') + D.assert_called_once_with( + id=domain['id'], + filter={'resourceRecords': {'type': {"operation": "_= a"}}}, + mask=ANY) + + D.reset_mock() + A.return_value = [domain] + D.return_value = [records[0]] + self.dns_client.get_records('example.com', host='a') + D.assert_called_once_with( + id=domain['id'], + filter={'resourceRecords': {'host': {"operation": "_= a"}}}, + mask=ANY) + + D.reset_mock() + A.return_value = [domain] + D.return_value = records[3:5] + self.dns_client.get_records('example.com', data='a') + D.assert_called_once_with( + id=domain['id'], + filter={'resourceRecords': {'data': {"operation": "_= a"}}}, + mask=ANY) + + D.reset_mock() + A.return_value = [domain] + D.return_value = records[3:5] + self.dns_client.get_records('example.com', ttl='86400') + D.assert_called_once_with( + id=domain['id'], + filter={'resourceRecords': {'ttl': {"operation": 86400}}}, + mask=ANY) From 7c20b17d1d1a90b6ba02bafd31a782bf33b2d1c5 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 13 Jun 2013 16:24:25 -0500 Subject: [PATCH 0063/3227] Adds Logging to the Transport functions --- SoftLayer/transport.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SoftLayer/transport.py b/SoftLayer/transport.py index a10e9a625..35317e149 100644 --- a/SoftLayer/transport.py +++ b/SoftLayer/transport.py @@ -11,9 +11,12 @@ SpecViolation, MethodNotFound, InvalidMethodParameters, InternalError, ApplicationError, RemoteSystemError, TransportError) import xmlrpclib +import logging import requests import json +log = logging.getLogger(__name__) + def make_xml_rpc_api_call(uri, method, args=None, headers=None, http_headers=None, timeout=None, verbose=False): @@ -34,10 +37,12 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, payload = xmlrpclib.dumps(tuple(largs), methodname=method, allow_none=True) + log.info('POST %s' % (uri)) + log.debug(payload) response = requests.post(uri, data=payload, headers=http_headers, timeout=timeout) - + log.debug(response.content) response.raise_for_status() result = xmlrpclib.loads(response.content,)[0][0] return result @@ -72,6 +77,7 @@ def make_rest_api_call(method, url, http_headers=None, timeout=None): :param dict http_headers: HTTP headers to use for the request :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.raise_for_status() @@ -84,6 +90,7 @@ def make_rest_api_call(method, url, http_headers=None, timeout=None): except requests.RequestException, e: raise TransportError(0, str(e)) + log.debug(resp.content) if url.endswith('.json'): return json.loads(resp.content) else: From 6155647a33ee56c86a482117d7d2306d566b31d7 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 13 Jun 2013 17:47:36 -0500 Subject: [PATCH 0064/3227] Adds documentation page about using the CLI with CCIs --- docs/cli/cci.rst | 177 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 docs/cli/cci.rst diff --git a/docs/cli/cci.rst b/docs/cli/cci.rst new file mode 100644 index 000000000..60fda6ffb --- /dev/null +++ b/docs/cli/cci.rst @@ -0,0 +1,177 @@ +.. _cci: + +Working with Cloud Compute Instances +==================================== +Using the SoftLayer portal for ordering Cloud Compute Instances is fine but for a number of reasons it's sometimes to use the command-line. For this, you can use the SoftLayer command-line client to make administrative tasks quicker and easier. This page gives an intro to working with SoftLayer Cloud Compute Instances using the SoftLayer command-line client. + +.. note:: + + The following assumes that the client is already :ref:`configured with valid SoftLayer credentials`. + + +First, let's list the current Cloud Compute Instances with `sl cci list`. +:: + + $ sl cci list + :....:............:......:.......:........:............:............:....................: + : id : datacenter : host : cores : memory : primary_ip : backend_ip : active_transaction : + :....:............:......:.......:........:............:............:....................: + :....:............:......:.......:........:............:............:....................: + +We don't have any Cloud Compute Instances! Let's fix that. Before we can create a CCI, we need to know what options are available to me: RAM, CPU, operating systems, disk sizes, disk types, datacenters. Luckily, there's a simple command to do that, `sl cci create-options`. + +:: + + $ sl cci create-options + :.................:..............................................................................................: + : Name : Value : + :.................:..............................................................................................: + : datacenter : ams01,dal01,dal05,sea01,sjc01,sng01,wdc01 : + : cpus (private) : 1,2,4,8 : + : cpus (standard) : 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 : + : memory : 1024,2048,3072,4096,5120,6144,7168,8192,9216,10240,11264,12288,13312,14336,15360,16384,32768 : + : os (CENTOS) : CENTOS_5_32 : + : : CENTOS_5_64 : + : : CENTOS_6_32 : + : : CENTOS_6_64 : + : os (CLOUDLINUX) : CLOUDLINUX_5_32 : + : : CLOUDLINUX_5_64 : + : : CLOUDLINUX_6_32 : + : : CLOUDLINUX_6_64 : + : os (DEBIAN) : DEBIAN_5_32 : + : : DEBIAN_5_64 : + : : DEBIAN_6_32 : + : : DEBIAN_6_64 : + : : DEBIAN_7_32 : + : : DEBIAN_7_64 : + : os (REDHAT) : REDHAT_5_64 : + : : REDHAT_6_32 : + : : REDHAT_6_64 : + : os (UBUNTU) : UBUNTU_10_32 : + : : UBUNTU_10_64 : + : : UBUNTU_12_32 : + : : UBUNTU_12_64 : + : : UBUNTU_8_32 : + : : UBUNTU_8_64 : + : os (VYATTACE) : VYATTACE_6.5_64 : + : os (WIN) : WIN_2003-DC-SP2-1_32 : + : : WIN_2003-DC-SP2-1_64 : + : : WIN_2003-ENT-SP2-5_32 : + : : WIN_2003-ENT-SP2-5_64 : + : : WIN_2003-STD-SP2-5_32 : + : : WIN_2003-STD-SP2-5_64 : + : : WIN_2008-DC-R2_64 : + : : WIN_2008-DC-SP2_32 : + : : WIN_2008-DC-SP2_64 : + : : WIN_2008-ENT-R2_64 : + : : WIN_2008-ENT-SP2_32 : + : : WIN_2008-ENT-SP2_64 : + : : WIN_2008-STD-R2-SP1_64 : + : : WIN_2008-STD-R2_64 : + : : WIN_2008-STD-SP2_32 : + : : WIN_2008-STD-SP2_64 : + : : WIN_2012-DC_64 : + : : WIN_2012-STD_64 : + : : WIN_7-ENT_32 : + : : WIN_7-PRO_32 : + : : WIN_8-ENT_64 : + : local disk(0) : 25,100 : + : local disk(2) : 25,100,150,200,300 : + : san disk(0) : 25,100 : + : san disk(2) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : + : san disk(3) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : + : san disk(4) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : + : san disk(5) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : + : nic : 10,100,1000 : + :.................:..............................................................................................: + +Here's the command to create a 2-core, 1G memory, Ubuntu 12.04 hourly instance in the San Jose datacenter using the command `sl cci create`. + +:: + + $ sl cci create --host=example --domain=softlayer.com -c 2 -m 1024 -o UBUNTU_12_64 --hourly --datacenter sjc01 + This action will incur charges on your account. Continue? [y/N]: y + :.........:......................................: + : name : value : + :.........:......................................: + : id : 1234567 : + : created : 2013-06-13T08:29:44-06:00 : + : guid : 6e013cde-a863-46ee-8s9a-f806dba97c89 : + :.........:......................................: + + +With the last command, the Cloud Compute Instance has begun being created. It should instantly appear in your listing now. + +:: + + $ sl cci list + :.........:............:.......................:.......:........:................:..............:....................: + : id : datacenter : host : cores : memory : primary_ip : backend_ip : active_transaction : + :.........:............:.......................:.......:........:................:..............:....................: + : 1234567 : sjc01 : example.softlayer.com : 2 : 1G : 108.168.200.11 : 10.54.80.200 : Assign Host : + :.........:............:.......................:.......:........:................:..............:....................: + +Cool. You may ask "It's creating... but how do I know when it's done?". Well, here's how: + +:: + + $ sl cci ready 'example' --wait=600 + READY + +When the previous command returns, I know that the Cloud Compute Instance has finished the provisioning process and is ready to use. This is *very* useful for chaining commands together. Now that you have your Cloud Compute Instance, let's get access to it. To do that, use the `sl cci detail` command. From the example below, you can see that the username is 'root' and password is 'ABCDEFGH'. + +.. warning:: + + Be careful when using the `--passwords` flag. This will print the password to the Cloud Compute Instance onto the screen. Make sure no one is looking over your shoulder. It's also advisable to change your root password soon after creating your Cloud Compute Instance. + +:: + + $ sl cci detail example --passwords + :..............:...........................: + : Name : Value : + :..............:...........................: + : id : 1234567 : + : hostname : example.softlayer.com : + : status : Active : + : state : Running : + : datacenter : sjc01 : + : cores : 2 : + : memory : 1G : + : public_ip : 108.168.200.11 : + : private_ip : 10.54.80.200 : + : os : Ubuntu : + : private_only : False : + : private_cpu : False : + : created : 2013-06-13T08:29:44-06:00 : + : modified : 2013-06-13T08:31:57-06:00 : + : users : root ABCDEFGH : + :..............:...........................: + + +There are many other commands to help manage Cloud Compute Instances. To see them all, use `sl help cci`. + +:: + + $ sl help cci + usage: sl cci [] [...] [options] + + Manage, delete, order compute instances + + The available commands are: + network Manage network settings + create Order and create a CCI + (see `sl cci create-options` for choices) + manage Manage active CCI + list List CCI's on the account + detail Output details about a CCI + dns DNS related actions to a CCI + cancel Cancel a running CCI + create-options Output available available options when creating a CCI + reload Reload the OS on a CCI based on its current configuration + ready Check if a CCI has finished provisioning + + For several commands, will be asked for. This can be the id, + hostname or the ip address for a CCI. + + Standard Options: + -h --help Show this screen From a9bd93d232313d0033c128a32285116973a993f1 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Fri, 14 Jun 2013 11:16:38 -0500 Subject: [PATCH 0065/3227] Major DNSManager refactor all ID fields now only take an int instead of str, use DNSManager.resolve_ids(str) Removed all references to search in the manager and cli, it was redundent CLI will now show tracebacks for unhandled errors --- SoftLayer/CLI/core.py | 6 +- SoftLayer/CLI/modules/dns.py | 75 ++++++++++------------ SoftLayer/managers/dns.py | 54 +++++++--------- SoftLayer/tests/CLI/core_tests.py | 12 ++++ SoftLayer/tests/managers/dns_tests.py | 89 ++++++++++++++------------- SoftLayer/utils.py | 37 +++++++---- 6 files changed, 140 insertions(+), 133 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 88bc4b7b0..895d7fd51 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -234,8 +234,12 @@ def main(args=sys.argv[1:], env=Environment()): exit_status = e.code except SystemExit, e: exit_status = e.code - except (SoftLayerError, Exception), e: + except SoftLayerError, e: env.err(str(e)) exit_status = 1 + except Exception, e: + import traceback + env.err(traceback.format_exc()) + exit_status = 1 sys.exit(exit_status) diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index b36a79269..d6c0bc68a 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -4,7 +4,6 @@ Manage DNS The available commands are: - search Look for a resource record by exact name edit Update resource records (bulk/single) create Create zone list List zones or a zone's records @@ -20,6 +19,18 @@ from SoftLayer import DNSManager, DNSZoneNotFound +def resolve_zone_id(dns_manager, name): + zone_ids = dns_manager.resolve_ids(name) + if len(zone_ids) == 0: + raise CLIAbort("Error: Unable to find zone '%s'" % name) + + if len(zone_ids) > 1: + raise CLIAbort("Error: More than one zone found for '%s': %s" % + name, ', '.join([str(_id) for _id in zone_ids])) + + return zone_ids[0] + + class DumpZone(CLIRunnable): """ usage: sl dns print [options] @@ -72,6 +83,9 @@ class DeleteZone(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) + + zone_id = resolve_zone_id(manager, args['']) + if args['--really'] or no_going_back(args['']): zone_id = manager.get_zone(args['']) manager.delete_zone(zone_id) @@ -97,7 +111,7 @@ def execute(cls, client, args): if args['']: return cls.list_zone(client, args[''], args) - return cls.list_zones(client) + return cls.list_all_zones(client) @staticmethod def list_zone(client, zone, args): @@ -113,8 +127,10 @@ def list_zone(client, zone, args): t.align['record'] = 'r' t.align['value'] = 'l' + zone_id = resolve_zone_id(manager, args['']) + try: - records = manager.get_records(args[''], + records = manager.get_records(zone_id, type=args.get('--type'), host=args.get('--record'), ttl=args.get('--ttl'), @@ -134,7 +150,7 @@ def list_zone(client, zone, args): return t @staticmethod - def list_zones(client): + def list_all_zones(client): manager = DNSManager(client) zones = manager.list_zones() t = Table([ @@ -178,12 +194,11 @@ class AddRecord(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - try: - zone = manager.get_zone(args[''])['id'] - except DNSZoneNotFound: - raise CLIAbort("No zone found matching: %s" % args['']) + + zone_id = resolve_zone_id(manager, args['']) + manager.create_record( - zone, + zone_id, args[''], args[''], args[''], @@ -211,9 +226,12 @@ class EditRecord(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) + + zone_id = resolve_zone_id(manager, args['']) + try: results = manager.search_record( - args[''], + zone_id, args['']) except DNSZoneNotFound: raise CLIAbort("No zone found matching: %s" % args['']) @@ -226,39 +244,6 @@ def execute(client, args): manager.edit_record(r) -class RecordSearch(CLIRunnable): - """ -usage: sl dns search [options] - -Look for a resource record by exact name - -Arguments: - Zone name (softlayer.com) - Resource record (www) -""" - action = 'search' - - @staticmethod - def execute(client, args): - manager = DNSManager(client) - results = [] - try: - results = manager.search_record( - args[''], - args['']) - except DNSZoneNotFound: - raise CLIAbort("No zone found matching: %s" % args['']) - - t = Table(['id', 'type', 'ttl', 'data']) - - t.align['ttl'] = 'c' - - for r in results: - t.add_row([r['id'], r['type'], r['ttl'], r['data']]) - - return t - - class RecordRemove(CLIRunnable): """ usage: sl dns remove [--id=ID] [options] @@ -279,12 +264,14 @@ class RecordRemove(CLIRunnable): def execute(client, args): manager = DNSManager(client) + zone_id = resolve_zone_id(manager, args['']) + if args['--id']: records = [{'id': args['--id']}] else: try: records = manager.search_record( - args[''], + zone_id, args['']) except DNSZoneNotFound: raise CLIAbort("No zone found matching: %s" % args['']) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index b536a3899..bbdb3f764 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -9,10 +9,11 @@ from time import strftime from SoftLayer.exceptions import DNSZoneNotFound -from SoftLayer.utils import NestedDict, query_filter +from SoftLayer.utils import (NestedDict, query_filter, + IdentifierMixin) -class DNSManager(object): +class DNSManager(IdentifierMixin, object): """ Manage DNS zones. """ def __init__(self, client): @@ -24,6 +25,12 @@ def __init__(self, client): self.client = client self.service = self.client['Dns_Domain'] self.record = self.client['Dns_Domain_ResourceRecord'] + self.resolvers = [self._get_zone_id_from_name] + + def _get_zone_id_from_name(self, name): + results = self.client['Account'].getDomains( + filter={"domains": {"name": query_filter(name)}}) + return [x['id'] for x in results] def list_zones(self, **kwargs): """ Retrieve a list of all DNS zones. @@ -33,24 +40,21 @@ def list_zones(self, **kwargs): """ return self.client['Account'].getDomains(**kwargs) - def get_zone(self, zone, records=True): + def get_zone(self, zone_id, records=True): """ Get a zone and its records. :param zone: the zone name """ - zone = zone.lower() - mask = {} + mask = None if records: - mask = {'resourceRecords': {}} - results = self.client['Account'].getDomains( - filter={"domains": {"name": query_filter(zone)}}, - mask=mask) + mask = 'resourceRecords' + results = self.service.getObject(id=zone_id, mask=mask) try: return results[0] except IndexError: - raise DNSZoneNotFound(zone) + raise DNSZoneNotFound(zone_id) def create_zone(self, zone, serial=None): """ Create a zone for the specified zone. @@ -82,7 +86,7 @@ def edit_zone(self, zone): """ self.service.editObject(zone) - def create_record(self, id, record, type, data, ttl=60): + def create_record(self, zone_id, record, type, data, ttl=60): """ Create a resource record on a domain. :param integer id: the zone's ID @@ -93,34 +97,21 @@ def create_record(self, id, record, type, data, ttl=60): """ self.record.createObject({ - 'domainId': id, + 'domainId': zone_id, 'ttl': ttl, 'host': record, 'type': type, 'data': data}) - def delete_record(self, recordid): + def delete_record(self, record_id): """ Delete a resource record by its ID. :param integer id: the record's ID """ - self.record.deleteObject(id=recordid) - - def search_record(self, zone, record): - """ Search for records on a zone that match a specific name. - Useful for validating whether a record exists or that it has the - correct value. - - :param zone: the zone name in which to search. - :param record: the record name to search for - - """ - rrs = self.get_zone(zone)['resourceRecords'] - records = filter(lambda x: x['host'].lower() == record.lower(), rrs) - return records + self.record.deleteObject(id=record_id) - def get_records(self, zone, ttl=None, data=None, host=None, + def get_records(self, zone_id, ttl=None, data=None, host=None, type=None, **kwargs): """ List, and optionally filter, records within a zone. @@ -147,9 +138,8 @@ def get_records(self, zone, ttl=None, data=None, host=None, _filter['resourceRecords']['type'] = \ query_filter(type.lower()) - domain = self.get_zone(zone, records=False) results = self.service.getResourceRecords( - id=domain['id'], + id=zone_id, mask='id,expire,domainId,host,minimum,refresh,retry,' 'mxPriority,ttl,type,data,responsiblePerson', filter=_filter.to_dict(), @@ -167,10 +157,10 @@ def edit_record(self, record): """ self.record.editObject(record, id=record['id']) - def dump_zone(self, id): + def dump_zone(self, zone_id): """ Retrieve a zone dump in BIND format. :param integer id: The zone ID to dump """ - return self.service.getZoneFileContents(id=id) + return self.service.getZoneFileContents(id=zone_id) diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 4fe6b4bb0..32adb5c31 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -118,6 +118,18 @@ def test_value_key_errors(self): 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 + errors = [TypeError, RuntimeError, NameError, OSError, SystemError] + for err in errors: + m.reset_mock() + m.return_value = 'testing' + self.env.get_module_name.side_effect = err + self.assertRaises( + SystemExit, cli.core.main, args=['cci', 'list'], env=self.env) + m.assert_called_once_with() + class TestCommandParser(unittest.TestCase): def setUp(self): diff --git a/SoftLayer/tests/managers/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py index 373d1ae6d..202ee81f2 100644 --- a/SoftLayer/tests/managers/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -32,21 +32,49 @@ def test_list_zones(self): def test_get_zone(self): zone_list = [ - {'name': 'test-example.com'}, - {'name': 'example.com'}, + {'id': 98765, 'name': 'test-example.com'}, + {'id': 12345, 'name': 'example.com', "resourceRecords": ["test"]}, ] - # match - self.client['Account'].getDomains.return_value = [zone_list[1]] - res = self.dns_client.get_zone('example.com') + # match, with defaults + self.client['Account'].getObject.return_value = [zone_list[1]] + res = self.dns_client.get_zone(12345) self.assertEqual(res, zone_list[1]) + self.client['Account'].getObject.assert_called_once_with( + id=12345, + mask='resourceRecords') # no match - self.client['Account'].getDomains.return_value = [] + self.client['Account'].getObject.return_value = [] self.assertRaises( DNSZoneNotFound, self.dns_client.get_zone, - 'shouldnt-match.com') + 5) + + # No records masked in + self.client['Account'].getObject.reset_mock() + self.client['Account'].getObject.return_value = [zone_list[1]] + self.dns_client.get_zone(12345, records=False) + self.client['Account'].getObject.assert_called_once_with( + id=12345, + mask=None) + + def test_resolve_zone_name(self): + zone_list = [{'name': 'example.com', 'id': 12345}] + # matching domain + self.client['Account'].getDomains.return_value = zone_list + res = self.dns_client._get_zone_id_from_name('example.com') + self.assertEqual([12345], res) + self.client['Account'].getDomains.assert_called_once_with( + filter={"domains": {"name": {"operation": "_= example.com"}}}) + + # no matches + self.client['Account'].getDomains.reset_mock() + self.client['Account'].getDomains.return_value = [] + res = self.dns_client._get_zone_id_from_name('example.com') + self.assertEqual([], res) + self.client['Account'].getDomains.assert_called_once_with( + filter={"domains": {"name": {"operation": "_= example.com"}}}) def test_create_zone(self): call = self.client['Dns_Domain'].createObject @@ -87,20 +115,6 @@ def test_delete_record(self): f = self.client['Dns_Domain_ResourceRecord'].deleteObject f.assert_called_once_with(id=1) - def test_search_record(self): - - self.client['Account'].getDomains.return_value = [{ - 'name': 'example.com', - 'resourceRecords': [ - {'host': 'TEST1'}, - {'host': 'test2'}, - {'host': 'test3'}, - ] - }] - - res = self.dns_client.search_record('example.com', 'test1') - self.assertEqual(res, [{'host': 'TEST1'}]) - def test_edit_record(self): self.dns_client.edit_record({'id': 1, 'name': 'test'}) f = self.client['Dns_Domain_ResourceRecord'].editObject @@ -116,7 +130,6 @@ def test_dump_zone(self): f.assert_called_once_with(id=1) def test_get_record(self): - domain = {'id': 12345, 'name': 'example.com'} records = [ {'ttl': 7200, 'data': 'd', 'host': 'a', 'type': 'cname'}, {'ttl': 900, 'data': '1', 'host': 'b', 'type': 'a'}, @@ -126,53 +139,41 @@ def test_get_record(self): {'ttl': 600, 'data': 'b', 'host': 'f', 'type': 'txt'}, ] - A = self.client['Account'].getDomains D = self.client['Dns_Domain'].getResourceRecords - # Invalid domain - A.return_value = [] - self.assertRaises(DNSZoneNotFound, - self.dns_client.get_records, - 'example-not.com') - - # valid domain, but no records matching - A.return_value = [domain] + # maybe valid domain, but no records matching D.return_value = [] - self.assertEqual(self.dns_client.get_records('example.com'), + self.assertEqual(self.dns_client.get_records(12345), []) D.reset_mock() - A.return_value = [domain] D.return_value = [records[1]] - self.dns_client.get_records('example.com', type='a') + self.dns_client.get_records(12345, type='a') D.assert_called_once_with( - id=domain['id'], + id=12345, filter={'resourceRecords': {'type': {"operation": "_= a"}}}, mask=ANY) D.reset_mock() - A.return_value = [domain] D.return_value = [records[0]] - self.dns_client.get_records('example.com', host='a') + self.dns_client.get_records(12345, host='a') D.assert_called_once_with( - id=domain['id'], + id=12345, filter={'resourceRecords': {'host': {"operation": "_= a"}}}, mask=ANY) D.reset_mock() - A.return_value = [domain] D.return_value = records[3:5] - self.dns_client.get_records('example.com', data='a') + self.dns_client.get_records(12345, data='a') D.assert_called_once_with( - id=domain['id'], + id=12345, filter={'resourceRecords': {'data': {"operation": "_= a"}}}, mask=ANY) D.reset_mock() - A.return_value = [domain] D.return_value = records[3:5] - self.dns_client.get_records('example.com', ttl='86400') + self.dns_client.get_records(12345, ttl='86400') D.assert_called_once_with( - id=domain['id'], + id=12345, filter={'resourceRecords': {'ttl': {"operation": 86400}}}, mask=ANY) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 4984622b8..b506b2588 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -81,16 +81,29 @@ def resolve_ids(self, identifier): :param string identifier: identifying string + :returns list: """ - # Before doing anything, let's see if this is an integer - try: - return [int(identifier)] - except ValueError: - pass # It was worth a shot - - for resolver in self.resolvers: - ids = resolver(identifier) - if ids: - return ids - - return [] + + return resolve_ids(identifier, self.resolvers) + + +def resolve_ids(identifier, resolvers): + """ Resolves IDs given a list of functions + + :param string identifier: identifier string + :param list resolvers: a list of functions + :returns list: + """ + + # Before doing anything, let's see if this is an integer + try: + return [int(identifier)] + except ValueError: + pass # It was worth a shot + + for resolver in resolvers: + ids = resolver(identifier) + if ids: + return ids + + return [] From 95a2739cee3521cb64bdec7b4f51fecd933ef68a Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 14 Jun 2013 11:29:55 -0500 Subject: [PATCH 0066/3227] Adds link to CCI CLI Page --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index c088c90c9..21f2fcd28 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -46,6 +46,7 @@ Command-Line Interface :maxdepth: 2 cli + cli/cci cli/dev From 549cfff5fd3d493f296ae15b790fdafcf1cf08dd Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 14 Jun 2013 16:06:20 -0500 Subject: [PATCH 0067/3227] Adds resolve_id() helper Adds/uses helper for CLI modules that want to resolve user-input into one (and only one) id for whatever object they're want. Also adds the id resolver for the 'sl dns print' command --- SoftLayer/CLI/helpers.py | 25 +++++++++++++++- SoftLayer/CLI/modules/cci.py | 39 ++++++++----------------- SoftLayer/CLI/modules/dns.py | 45 ++++++++++------------------- SoftLayer/CLI/modules/hardware.py | 22 ++++---------- SoftLayer/managers/dns.py | 8 ++--- SoftLayer/managers/hardware.py | 6 ++-- SoftLayer/tests/CLI/helper_tests.py | 19 ++++++++++++ 7 files changed, 82 insertions(+), 82 deletions(-) diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index fa119d2da..b2083b940 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -12,7 +12,7 @@ __all__ = ['Table', 'CLIRunnable', 'FormattedItem', 'valid_response', 'confirm', 'no_going_back', 'mb_to_gb', 'gb', 'listing', 'CLIAbort', - 'NestedDict'] + 'NestedDict', 'resolve_id'] class FormattedItem(object): @@ -29,6 +29,29 @@ def __str__(self): __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 + + """ + 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)) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 5de8f08ce..9202d0876 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -30,22 +30,7 @@ CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, FormattedItem) from SoftLayer.CLI.helpers import ( - CLIAbort, ArgumentError, SequentialOutput, - NestedDict, blank) - - -def resolve_id(cci_manager, identifier): - cci_ids = cci_manager.resolve_ids(identifier) - - if len(cci_ids) == 0: - raise CLIAbort("Error: Unable to find CCI '%s'" % identifier) - - if len(cci_ids) > 1: - raise CLIAbort( - "Error: Multiple CCIs found for '%s': %s" % - (identifier, ', '.join([str(_id) for _id in cci_ids]))) - - return cci_ids[0] + CLIAbort, ArgumentError, SequentialOutput, NestedDict, blank, resolve_id) class ListCCIs(CLIRunnable): @@ -143,7 +128,7 @@ def execute(client, args): t.align['Name'] = 'r' t.align['Value'] = 'l' - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') result = cci.get_instance(cci_id) result = NestedDict(result) @@ -497,7 +482,7 @@ class ReadyCCI(CLIRunnable): def execute(client, args): cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') ready = cci.wait_for_transaction(cci_id, int(args.get('--wait') or 0)) if ready: @@ -519,7 +504,7 @@ class ReloadCCI(CLIRunnable): @staticmethod def execute(client, args): cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') if args['--really'] or no_going_back(cci_id): cci.reload_instance(cci_id) else: @@ -539,7 +524,7 @@ class CancelCCI(CLIRunnable): @staticmethod def execute(client, args): cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') if args['--really'] or no_going_back(cci_id): cci.cancel_instance(cci_id) else: @@ -580,7 +565,7 @@ def execute(cls, client, args): def exec_shutdown(client, args): vg = client['Virtual_Guest'] cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') if args['--soft']: result = vg.powerOffSoft(id=cci_id) elif args['--cycle']: @@ -594,28 +579,28 @@ def exec_shutdown(client, args): def exec_poweron(client, args): vg = client['Virtual_Guest'] cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') return vg.powerOn(id=cci_id) @staticmethod def exec_pause(client, args): vg = client['Virtual_Guest'] cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') return vg.pause(id=cci_id) @staticmethod def exec_resume(client, args): vg = client['Virtual_Guest'] cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') return vg.resume(id=cci_id) @staticmethod def exec_reboot(client, args): vg = client['Virtual_Guest'] cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') if args['--cycle']: result = vg.rebootHard(id=cci_id) elif args['--soft']: @@ -658,7 +643,7 @@ def exec_port(client, args): func = vg.setPrivateNetworkInterfaceSpeed cci = CCIManager(client) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') result = func(args['--speed'], id=cci_id) if result: @@ -741,7 +726,7 @@ def sync_ptr_record(): instance['fullyQualifiedDomainName'], ttl=7200) - cci_id = resolve_id(cci, args.get('')) + cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') instance = cci.get_instance(cci_id) if not instance['primaryIpAddress']: diff --git a/SoftLayer/CLI/modules/dns.py b/SoftLayer/CLI/modules/dns.py index d6c0bc68a..61e171837 100755 --- a/SoftLayer/CLI/modules/dns.py +++ b/SoftLayer/CLI/modules/dns.py @@ -15,22 +15,11 @@ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. -from SoftLayer.CLI import CLIRunnable, no_going_back, Table, CLIAbort +from SoftLayer.CLI import ( + CLIRunnable, no_going_back, Table, CLIAbort, resolve_id) from SoftLayer import DNSManager, DNSZoneNotFound -def resolve_zone_id(dns_manager, name): - zone_ids = dns_manager.resolve_ids(name) - if len(zone_ids) == 0: - raise CLIAbort("Error: Unable to find zone '%s'" % name) - - if len(zone_ids) > 1: - raise CLIAbort("Error: More than one zone found for '%s': %s" % - name, ', '.join([str(_id) for _id in zone_ids])) - - return zone_ids[0] - - class DumpZone(CLIRunnable): """ usage: sl dns print [options] @@ -45,8 +34,9 @@ class DumpZone(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) + zone_id = resolve_id(manager.resolve_ids, args[''], name='zone') try: - return manager.dump_zone(manager.get_zone(args[''])['id']) + return manager.dump_zone(zone_id) except DNSZoneNotFound: raise CLIAbort("No zone found matching: %s" % args['']) @@ -83,11 +73,9 @@ class DeleteZone(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - - zone_id = resolve_zone_id(manager, args['']) + zone_id = resolve_id(manager.resolve_ids, args[''], name='zone') if args['--really'] or no_going_back(args['']): - zone_id = manager.get_zone(args['']) manager.delete_zone(zone_id) raise CLIAbort("Aborted.") @@ -127,15 +115,16 @@ def list_zone(client, zone, args): t.align['record'] = 'r' t.align['value'] = 'l' - zone_id = resolve_zone_id(manager, args['']) + zone_id = resolve_id(manager.resolve_ids, args[''], name='zone') try: - records = manager.get_records(zone_id, - type=args.get('--type'), - host=args.get('--record'), - ttl=args.get('--ttl'), - data=args.get('--data'), - ) + records = manager.get_records( + zone_id, + type=args.get('--type'), + host=args.get('--record'), + ttl=args.get('--ttl'), + data=args.get('--data'), + ) except DNSZoneNotFound: raise CLIAbort("No zone found matching: %s" % args['']) @@ -195,7 +184,7 @@ class AddRecord(CLIRunnable): def execute(client, args): manager = DNSManager(client) - zone_id = resolve_zone_id(manager, args['']) + zone_id = resolve_id(manager.resolve_ids, args[''], name='zone') manager.create_record( zone_id, @@ -226,8 +215,7 @@ class EditRecord(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - - zone_id = resolve_zone_id(manager, args['']) + zone_id = resolve_id(manager.resolve_ids, args[''], name='zone') try: results = manager.search_record( @@ -263,8 +251,7 @@ class RecordRemove(CLIRunnable): @staticmethod def execute(client, args): manager = DNSManager(client) - - zone_id = resolve_zone_id(manager, args['']) + zone_id = resolve_id(manager.resolve_ids, args[''], name='zone') if args['--id']: records = [{'id': args['--id']}] diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 6224faffb..abd2923ed 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -14,24 +14,10 @@ """ from SoftLayer.CLI.helpers import ( CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, - gb, no_going_back) + gb, no_going_back, resolve_id) from SoftLayer import HardwareManager -def resolve_id(manager, identifier): - ids = manager.resolve_ids(identifier) - - if len(ids) == 0: - raise CLIAbort("Error: Unable to find hardware '%s'" % identifier) - - if len(ids) > 1: - raise CLIAbort( - "Error: Multiple hardware found for '%s': %s" % - (identifier, ', '.join([str(_id) for _id in ids]))) - - return ids[0] - - class ListHardware(CLIRunnable): """ usage: sl hardware list [options] @@ -124,7 +110,8 @@ def execute(client, args): t.align['Name'] = 'r' t.align['Value'] = 'l' - hardware_id = resolve_id(hardware, args.get('')) + hardware_id = resolve_id( + hardware.resolve_ids, args.get(''), 'hardware') result = hardware.get_hardware(hardware_id) result = NestedDict(result) @@ -189,7 +176,8 @@ class HardwareReload(CLIRunnable): @staticmethod def execute(client, args): hardware = HardwareManager(client) - hardware_id = resolve_id(hardware, args.get('')) + hardware_id = resolve_id( + hardware.resolve_ids, args.get(''), 'hardware') if args['--really'] or no_going_back(hardware_id): hardware.reload(hardware_id) else: diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index bbdb3f764..a98023934 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -9,8 +9,7 @@ from time import strftime from SoftLayer.exceptions import DNSZoneNotFound -from SoftLayer.utils import (NestedDict, query_filter, - IdentifierMixin) +from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin class DNSManager(IdentifierMixin, object): @@ -135,15 +134,14 @@ def get_records(self, zone_id, ttl=None, data=None, host=None, _filter['resourceRecords']['data'] = query_filter(data) if type: - _filter['resourceRecords']['type'] = \ - query_filter(type.lower()) + _filter['resourceRecords']['type'] = query_filter(type.lower()) results = self.service.getResourceRecords( id=zone_id, mask='id,expire,domainId,host,minimum,refresh,retry,' 'mxPriority,ttl,type,data,responsiblePerson', filter=_filter.to_dict(), - ) + ) return results diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index f2968161d..20c5e422d 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -117,10 +117,10 @@ def get_hardware(self, id, **kwargs): 'primaryBackendIpAddress', 'primaryIpAddress', 'datacenter.name', - 'networkComponents[id, status, maxSpeed, name,' \ - 'ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,'\ + 'networkComponents[id, status, maxSpeed, name,' + 'ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,' 'port, primarySubnet]', - 'networkComponents.primarySubnet[id, netmask,' \ + 'networkComponents.primarySubnet[id, netmask,' 'broadcastAddress, networkIdentifier, gateway]', 'activeTransaction.id', 'operatingSystem.softwareLicense.' diff --git a/SoftLayer/tests/CLI/helper_tests.py b/SoftLayer/tests/CLI/helper_tests.py index 29a2b422c..fef62c6a0 100644 --- a/SoftLayer/tests/CLI/helper_tests.py +++ b/SoftLayer/tests/CLI/helper_tests.py @@ -156,3 +156,22 @@ class TestCommand(cli.CLIRunnable): self.assertEqual( cli.environment.CLIRunnableType.env.plugins, {'helper_tests': {'test': TestCommand}}) + + +class ResolveIdTests(unittest.TestCase): + + def test_resolve_id_one(self): + resolver = lambda r: [12345] + id = cli.helpers.resolve_id(resolver, 'test') + + self.assertEqual(id, 12345) + + def test_resolve_id_none(self): + resolver = lambda r: [] + self.assertRaises( + cli.helpers.CLIAbort, cli.helpers.resolve_id, resolver, 'test') + + def test_resolve_id_multiple(self): + resolver = lambda r: [12345, 54321] + self.assertRaises( + cli.helpers.CLIAbort, cli.helpers.resolve_id, resolver, 'test') From 879177afd7ffcb1756b31706bfdb714ce8ee4566 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 14 Jun 2013 20:31:37 -0500 Subject: [PATCH 0068/3227] Adds Token-based Auth This adds the ability to use username/password based auth to get an auth token to use for subsequent calls. import SoftLayer client = SoftLayer.Client() client.authenticate_with_password('username', 'password') import SoftLayer client = SoftLayer.Client() # Get all security questions (you basically pick one at random that you have answered) client['User_Security_Question'].getAllObjects() client.authenticate_with_password('username', 'password', 122, '2004') note: this is pending an API release to add this feature to the XML-RPC API. --- SoftLayer/API.py | 50 +++++++++++++++++++++++--- SoftLayer/__init__.py | 3 +- SoftLayer/tests/api_tests.py | 50 ++++++++++++++++++++++++++ SoftLayer/tests/transport_tests.py | 56 +++++++++++++++++++++++++----- 4 files changed, 145 insertions(+), 14 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index aefb660b4..fa4039054 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -12,6 +12,9 @@ from SoftLayer.exceptions import SoftLayerError import os +__all__ = ['Client', 'BasicAuthentication', 'TokenAuthentication', + 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] + API_USERNAME = None API_KEY = None API_BASE_URL = API_PUBLIC_ENDPOINT @@ -31,6 +34,24 @@ 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 @@ -90,8 +111,9 @@ def __init__(self, service_name=None, id=None, username=None, api_key=None, username = username or API_USERNAME or \ os.environ.get('SL_USERNAME') or '' api_key = api_key or API_KEY or os.environ.get('SL_API_KEY') or '' - self.auth = BasicAuthentication(username, api_key) - self.set_authentication(username, api_key) + if username and api_key: + self.auth = BasicAuthentication(username, api_key) + self.set_authentication(username, api_key) if id is not None: self.set_init_parameter(int(id)) @@ -100,6 +122,26 @@ def __init__(self, service_name=None, id=None, username=None, api_key=None, API_PUBLIC_ENDPOINT).rstrip('/') self.timeout = timeout + def authenticate_with_password(self, username, password, + security_question_id=None, + security_question_answer=None): + """ Performs Username/Password Authentication and gives back an auth + handler to use to create a client that uses token-based auth. + + :param string username: your SoftLayer username + :param string password: your SoftLayer password + :param int security_question_id: The security question id to answer + :param string security_question_answer: The answer to the security + question + + """ + res = self['User_Customer'].getPortalLoginToken( + username, + password, + security_question_id, + security_question_answer) + self.auth = TokenAuthentication(res['userId'], res['hash']) + def add_raw_header(self, name, value): """ Set HTTP headers for API calls. @@ -261,12 +303,12 @@ def call(self, service, method, *args, **kwargs): objectid = kwargs.get('id') objectmask = kwargs.get('mask') objectfilter = kwargs.get('filter') - headers = kwargs.get('headers') + headers = kwargs.get('headers', {}) raw_headers = kwargs.get('raw_headers') limit = kwargs.get('limit') offset = kwargs.get('offset', 0) - if headers is None: + if not headers and self.auth: headers = self.auth.get_headers() http_headers = { diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 3ff4ea1de..2d470535f 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -16,8 +16,7 @@ """ from SoftLayer.consts import VERSION -from API import ( - Client, BasicAuthentication, API_PUBLIC_ENDPOINT, API_PRIVATE_ENDPOINT) +from API import * # NOQA from managers import * # NOQA from SoftLayer.exceptions import * # NOQA diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 55eb7f0d4..876f8ef18 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -349,6 +349,32 @@ def test_call_invalid_arguments(self): self.client.call, 'SERVICE', 'METHOD', invalid_kwarg='invalid') +class UnauthenticatedAPIClient(unittest.TestCase): + def setUp(self): + self.client = SoftLayer.Client(endpoint_url="ENDPOINT") + + def test_init(self): + self.assertIsNone(self.client.auth) + + @patch('SoftLayer.API.Client.call') + def test_authenticate_with_password(self, _call): + _call.return_value = { + 'userId': 12345, + 'hash': 'TOKEN', + } + self.client.authenticate_with_password('USERNAME', 'PASSWORD') + _call.assert_called_with( + 'User_Customer', + 'getPortalLoginToken', + 'USERNAME', + 'PASSWORD', + None, + None) + 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() @@ -375,3 +401,27 @@ 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/transport_tests.py b/SoftLayer/tests/transport_tests.py index f9ad470bd..19981574e 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -12,19 +12,59 @@ from mock import patch, MagicMock from SoftLayer import SoftLayerAPIError, TransportError -from SoftLayer.transport import make_rest_api_call +from SoftLayer.transport import make_rest_api_call, make_xml_rpc_api_call from requests import HTTPError, RequestException +class TestXmlRpcAPICall(unittest.TestCase): + + @patch('SoftLayer.transport.requests.post') + def test_call(self, post): + post().content = ''' + + + + + + + + + +''' + + data = ''' + +getObject + + + + +headers + + + + + +''' + resp = make_xml_rpc_api_call( + 'http://something.com/path/to/resource', 'getObject') + self.assertEqual(resp, []) + post.assert_called_with( + 'http://something.com/path/to/resource', + data=data, + headers=None, + timeout=None,) + + class TestRestAPICall(unittest.TestCase): @patch('SoftLayer.transport.requests.request') def test_json(self, request): request().content = '{}' - resp = make_rest_api_call('GET', 'http://something.com/path/to/resourse.json') + resp = make_rest_api_call('GET', 'http://something.com/path/to/resource.json') self.assertEqual(resp, {}) request.assert_called_with( - 'GET', 'http://something.com/path/to/resourse.json', + 'GET', 'http://something.com/path/to/resource.json', headers=None, timeout=None) @@ -42,15 +82,15 @@ def test_json(self, request): SoftLayerAPIError, make_rest_api_call, 'GET', - 'http://something.com/path/to/resourse.json') + 'http://something.com/path/to/resource.json') @patch('SoftLayer.transport.requests.request') def test_text(self, request): request().text = 'content' - resp = make_rest_api_call('GET', 'http://something.com/path/to/resourse.txt') + resp = make_rest_api_call('GET', 'http://something.com/path/to/resource.txt') self.assertEqual(resp, 'content') request.assert_called_with( - 'GET', 'http://something.com/path/to/resourse.txt', + 'GET', 'http://something.com/path/to/resource.txt', headers=None, timeout=None) @@ -65,7 +105,7 @@ def test_text(self, request): SoftLayerAPIError, make_rest_api_call, 'GET', - 'http://something.com/path/to/resourse.txt') + 'http://something.com/path/to/resource.txt') @patch('SoftLayer.transport.requests.request') def test_unknown_error(self, request): @@ -79,4 +119,4 @@ def test_unknown_error(self, request): TransportError, make_rest_api_call, 'GET', - 'http://something.com/path/to/resourse.txt') + 'http://something.com/path/to/resource.txt') From 9a46e0b1670e1be124625c4ad5584a60ece9f3e1 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 19 Jun 2013 12:05:18 -0500 Subject: [PATCH 0069/3227] Fixes a test --- SoftLayer/consts.py | 1 + SoftLayer/tests/api_tests.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 0b6a68321..5550693b9 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -9,5 +9,6 @@ VERSION = 'v2.2.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/' API_PRIVATE_ENDPOINT_REST = 'https://api.service.softlayer.com/rest/v3/' USER_AGENT = "SoftLayer Python %s" % VERSION diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 876f8ef18..18e342cda 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -353,8 +353,10 @@ class UnauthenticatedAPIClient(unittest.TestCase): def setUp(self): self.client = SoftLayer.Client(endpoint_url="ENDPOINT") + @patch.dict('os.environ', {'SL_USERNAME': '', 'SL_API_KEY': ''}) def test_init(self): - self.assertIsNone(self.client.auth) + client = SoftLayer.Client() + self.assertIsNone(client.auth) @patch('SoftLayer.API.Client.call') def test_authenticate_with_password(self, _call): From a7ab5ed8e3d0ffdb59d74ff5b12baa632eb977cc Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Thu, 20 Jun 2013 15:41:24 -0500 Subject: [PATCH 0070/3227] Adds post install hook support to CCI CLI/Manager --- SoftLayer/CLI/modules/cci.py | 13 +++++++++---- SoftLayer/managers/cci.py | 5 ++++- SoftLayer/tests/managers/cci_tests.py | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 5de8f08ce..8c5d73116 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -358,6 +358,8 @@ class CreateCCI(CLIRunnable): -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) --wait=SECONDS Block until CCI is finished provisioning for up to X seconds before returning. """ @@ -401,16 +403,16 @@ def execute(client, args): data["memory"] = memory if args['--monthly']: - data["hourly"] = False + data['hourly'] = False if args.get('--os'): - data["os_code"] = args['--os'] + data['os_code'] = args['--os'] if args.get('--image'): - data["image_id"] = args['--image'] + data['image_id'] = args['--image'] if args.get('--datacenter'): - data["datacenter"] = args['--datacenter'] + data['datacenter'] = args['--datacenter'] if args.get('--network'): data['nic_speed'] = args.get('--network') @@ -424,6 +426,9 @@ def execute(client, args): finally: f.close() + if args.get('--postinstall'): + data['post_uri'] = args.get('--postinstall') + t = Table(['Item', 'cost']) t.align['Item'] = 'r' t.align['cost'] = 'r' diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 5295c4550..bf01f8932 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -176,7 +176,7 @@ def _generate_create_dict( 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): + userdata=None, nic_speed=None, disks=None, post_uri=None): required = [cpus, memory, hostname, domain] @@ -244,6 +244,9 @@ def _generate_create_dict( } ) + if post_uri: + data['postInstallScriptUri'] = post_uri + return data def wait_for_transaction(self, id, limit, delay=1): diff --git a/SoftLayer/tests/managers/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py index 151c5364b..72d9dd838 100644 --- a/SoftLayer/tests/managers/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -351,6 +351,29 @@ def test_generate_network(self): self.assertEqual(data, assert_data) + def test_generate_post_uri(self): + data = self.cci._generate_create_dict( + cpus=1, + memory=1, + hostname='test', + domain='example.com', + os_code="STRING", + post_uri='https://example.com/boostrap.sh', + ) + + assert_data = { + 'startCpus': 1, + 'maxMemory': 1, + 'hostname': 'test', + 'domain': 'example.com', + 'localDiskFlag': True, + 'operatingSystemReferenceCode': "STRING", + 'hourlyBillingFlag': True, + 'postInstallScriptUri': 'https://example.com/boostrap.sh', + } + + self.assertEqual(data, assert_data) + def test_generate_no_disks(self): data = self.cci._generate_create_dict( cpus=1, From 1dd8612e73ff8a6ac9651121d4e07600c4979337 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 21 Jun 2013 15:05:52 -0500 Subject: [PATCH 0071/3227] Adds debugging to the CLI Note: This only works with a full command. For example: sl cci list --debug=3 Different levels: 1=warn, 2=info, 3=debug. The default is only critical log events. All logging will go to stderr. --- SoftLayer/CLI/core.py | 24 ++++++++++++++++++++---- SoftLayer/tests/CLI/core_tests.py | 14 +++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 895d7fd51..63cc7f21d 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -30,6 +30,7 @@ import sys import os import os.path +import logging from prettytable import FRAME, NONE from docopt import docopt, DocoptExit @@ -42,6 +43,14 @@ Environment, CLIRunnableType, InvalidCommand, InvalidModule) +DEBUG_LOGGING_MAP = { + '0': logging.CRITICAL, + '1': logging.WARNING, + '2': logging.INFO, + '3': logging.DEBUG +} + + def format_output(data, fmt='table'): if isinstance(data, basestring): return data @@ -101,10 +110,7 @@ def get_main_help(self): def get_module_help(self, module_name): module = self.env.load_module(module_name) - arg_doc = module.__doc__ + """ -Standard Options: - -h --help Show this screen -""" + arg_doc = module.__doc__ return arg_doc.strip() def get_command_help(self, module_name, command_name): @@ -127,6 +133,8 @@ def get_command_help(self, module_name, command_name): Standard Options: --format=ARG Output format. [Options: table, raw] [Default: %s] -C FILE --config=FILE Config file location. [Default: ~/.softlayer] + --debug=LEVEL Specifies the debug noise level + 1=warn, 2=info, 3=debug -h --help Show this screen """ % default_format return arg_doc.strip() @@ -185,6 +193,14 @@ def main(args=sys.argv[1:], env=Environment()): try: command, command_args = resolver.parse(args) + # Set logging level + debug_level = command_args.get('--debug') + if debug_level: + logger = logging.getLogger() + h = logging.StreamHandler() + logger.addHandler(h) + logger.setLevel(DEBUG_LOGGING_MAP.get(debug_level, logging.DEBUG)) + # Parse Config config_files = ["~/.softlayer"] diff --git a/SoftLayer/tests/CLI/core_tests.py b/SoftLayer/tests/CLI/core_tests.py index 32adb5c31..d1634ebea 100644 --- a/SoftLayer/tests/CLI/core_tests.py +++ b/SoftLayer/tests/CLI/core_tests.py @@ -64,7 +64,8 @@ def test_normal_path(self): self.env.get_module_name.return_value = 'cci' self.assertRaises( SystemExit, cli.core.main, - args=['cci', 'list', '--config=path/to/config'], env=self.env) + args=['cci', 'list', '--config=path/to/config'], + env=self.env) self.assertRaises( SystemExit, cli.core.main, args=['cci', 'nope', '--config=path/to/config'], env=self.env) @@ -72,6 +73,17 @@ def test_normal_path(self): SystemExit, cli.core.main, args=['cci', 'list', '--format=totallynotvalid'], env=self.env) + @patch('logging.getLogger') + @patch('logging.StreamHandler') + def test_with_debug(self, stream_handler, logger): + self.env.get_module_name.return_value = 'cci' + self.assertRaises( + SystemExit, cli.core.main, + args=['cci', 'list', '--debug=3'], + env=self.env) + logger().setLevel.assert_called_with(10) + logger().addHandler.assert_called_with(stream_handler()) + def test_invalid_module(self): self.env.get_module_name.return_value = 'nope' self.assertRaises( From c7f8c080ac6fdfc53b56111c6391542b4314d157 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 24 Jun 2013 10:57:30 -0500 Subject: [PATCH 0072/3227] Add hardware ordering for bare metal instances. --- SoftLayer/CLI/modules/hardware.py | 56 ++++++ SoftLayer/managers/hardware.py | 211 +++++++++++++++++++++ SoftLayer/tests/managers/hardware_tests.py | 175 ++++++++++++++++- 3 files changed, 440 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index abd2923ed..9cab895da 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -8,6 +8,8 @@ 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 For several commands, will be asked for. This can be the id, hostname or the ip address for a piece of hardware. @@ -182,3 +184,57 @@ def execute(client, args): hardware.reload(hardware_id) else: CLIAbort('Aborted') + + +class CancelHardware(CLIRunnable): + """ +usage: sl hardware cancel [options] + +Cancel a dedicated server + +Options: + --reason An optional cancellation reason. See cancel-reasons for a list of + available options. +""" + + action = 'cancel' + options = ['confirm'] + + @staticmethod + def execute(client, args): + hw = HardwareManager(client) + hw_id = resolve_id(hw, args.get('')) + + print "(Optional) Add a cancellation comment:", + comment = raw_input() + + reason = args.get('--reason') + + if args['--really'] or no_going_back(hw_id): + hw.cancel_hardware(hw_id, reason, comment) + else: + CLIAbort('Aborted') + + +class HardwareCancelReasons(CLIRunnable): + """ +usage: sl hardware cancel-reasons + +Display a list of cancellation reasons +""" + + action = 'cancel-reasons' + + @staticmethod + def execute(client, args): + t = Table(['Code', 'Reason']) + t.align['Code'] = 'r' + t.align['Reason'] = 'l' + + mgr = HardwareManager(client) + reasons = mgr.get_cancellation_reasons().iteritems() + + for code, reason in reasons: + t.add_row([code, reason]) + + return t diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 20c5e422d..4be2e9ed8 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -25,6 +25,53 @@ def __init__(self, client): self.account = self.client['Account'] self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] + 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. + """ + + reasons = self.get_cancellation_reasons() + cancel_reason = reasons['unneeded'] + + if reason in reasons: + cancel_reason = reasons[reason] + + # Arguments per SLDN: + # attachmentId - Hardware ID + # Reason + # content - Comment about the cancellation + # cancelAssociatedItems + # attachmentType - Only option is HARDWARE + ticket_obj = self.client['Ticket'] + return ticket_obj.createCancelServerTicket(id, cancel_reason, + comment, True, + 'HARDWARE') + + def cancel_metal(self, id, immediate=False): + """ Cancels the specified bare metal instance. + + :param int id: The ID of the bare metal instance to be cancelled. + :param bool immediate: If true, the bare metal instance will be + cancelled immediately. Otherwise, it will be + scheduled to cancel on the anniversary date. + """ + hw_billing = self.get_hardware(id=id, + mask='mask[id, billingItem.id]') + + billing_id = hw_billing['billingItem']['id'] + + billing_item = self.client['Billing_Item'] + + if immediate: + return billing_item.cancelService(id=billing_id) + else: + return billing_item.cancelServiceOnAnniversaryDate(id=billing_id) + 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): @@ -47,6 +94,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, 'id', 'hostname', 'domain', + 'hardwareStatusId', 'globalIdentifier', 'fullyQualifiedDomainName', 'processorCoreAmount', @@ -95,6 +143,72 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, kwargs['filter'] = _filter.to_dict() return self.account.getHardware(**kwargs) + 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. + """ + hw_id = self._get_bare_metal_package_id() + + if not hw_id: + return None + + package = self.client['Product_Package'] + + results = { + 'categories': {}, + 'locations': [] + } + + # 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=hw_id) + + for loc in object_data: + details = loc['location']['locationPackageDetails'][0] + + results['locations'].append({ + 'delivery_information': details.get('deliveryTimeInformation'), + 'keyname': loc['keyname'], + 'long_name': loc['description'], + }) + + for config in package.getConfiguration(id=hw_id, + mask='mask[itemCategory]'): + code = config['itemCategory']['categoryCode'] + category = { + 'sort': config['sort'], + 'step': config['orderStepId'], + 'is_required': config['isRequired'], + 'name': config['itemCategory']['name'], + 'items': [], + } + + results['categories'][code] = category + + # Now pull in the available package item + for item in package.getItems(id=hw_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'], + # TODO - Deal with multiple prices properly. + 'prices': item['prices'], + 'sort': item['prices'][0]['sort'], + 'price_id': item['prices'][0]['id'], + 'capacity': int(item.get('capacity') or 0), + }) + + return results + def get_hardware(self, id, **kwargs): """ Get details about a hardware device @@ -143,6 +257,103 @@ def reload(self, id): return self.hardware.reloadCurrentOperatingSystemConfiguration( 'FORCE', id=id) + def place_order(self, **kwargs): + create_options = self._generate_create_dict(**kwargs) + return self.client['Product_Order'].placeOrder(create_options) + + def verify_order(self, **kwargs): + create_options = self._generate_create_dict(**kwargs) + return self.client['Product_Order'].verifyOrder(create_options) + + def get_cancellation_reasons(self): + return { + 'unneeded': 'No longer needed', + 'closing': 'Business closing down', + 'cost': 'Server / Upgrade Costs', + 'migrate_larger': 'Migrating to larger server', + 'migrate_smaller': 'Migrating to smaller server', + 'datacenter': 'Migrating to a different SoftLayer datacenter', + 'performance': 'Network performance / latency', + 'support': 'Support response / timing', + 'sales': 'Sales process / upgrades', + 'moving': 'Moving to competitor', + } + + def _generate_create_dict( + self, server_core=None, hourly=True, + hostname=None, domain=None, disk0=None, + location=None, os=None, image_id=None, + pri_ip_addresses=None, bandwidth=None, + userdata=None, monitoring=None, port_speed=None, + vulnerability_scanner=None, response=None, + vpn_management=None, remote_management=None, + notification=None, bare_metal=True, database=None): + + order = { + 'hardware': [{ + 'bareMetalInstanceFlag': bare_metal, + 'hostname': hostname, + 'domain': domain, + }], + 'location': location, + 'prices': [ + ], + } + + if bare_metal: + order['packageId'] = self._get_bare_metal_package_id() + + if server_core: + order['prices'].append({'id': int(server_core)}) + + if disk0: + order['prices'].append({'id': int(disk0)}) + + if os: + order['prices'].append({'id': int(os)}) + + if pri_ip_addresses: + order['prices'].append({'id': int(pri_ip_addresses)}) + + if bandwidth: + order['prices'].append({'id': int(bandwidth)}) + + if monitoring: + order['prices'].append({'id': int(monitoring)}) + + if port_speed: + order['prices'].append({'id': int(port_speed)}) + + if vulnerability_scanner: + order['prices'].append({'id': int(vulnerability_scanner)}) + + if response: + order['prices'].append({'id': int(response)}) + + if vpn_management: + order['prices'].append({'id': int(vpn_management)}) + + if remote_management: + order['prices'].append({'id': int(remote_management)}) + + if notification: + order['prices'].append({'id': int(notification)}) + + return order + + def _get_bare_metal_package_id(self): + packages = self.client['Product_Package'].getAllObjects( + mask='mask[id, name]', + filter={'name': query_filter('Bare Metal Instance')}) + + hw_id = 0 + for package in packages: + if 'Bare Metal Instance' == package['name']: + hw_id = package['id'] + break + + return hw_id + def _get_ids_from_hostname(self, hostname): results = self.list_hardware(hostname=hostname, mask="id") return [result['id'] for result in results] diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index f01b8d307..93b7a918c 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -11,7 +11,7 @@ import unittest2 as unittest except ImportError: import unittest # NOQA -from mock import MagicMock, ANY, call +from mock import MagicMock, ANY, call, patch class HardwareTests(unittest.TestCase): @@ -80,7 +80,7 @@ def test_resolve_ids_hostname(self): _id = self.hardware._get_ids_from_hostname('hostname') self.assertEqual(_id, ['1234']) - def test_get_instance(self): + def test_get_hardware(self): self.client.__getitem__().getObject.return_value = { 'hourlyVirtualGuests': "this is unique"} self.hardware.get_hardware(1) @@ -91,3 +91,174 @@ def test_reload(self): self.hardware.reload(id=1) f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration f.assert_called_once_with('FORCE', id=1) + + def test_get_bare_metal_create_options_returns_none_on_error(self): + self.client['Product_Package'].getAllObjects.return_value = [ + {'name': 'No Matching Instances', 'id': 0}] + + self.assertIsNone(self.hardware.get_bare_metal_create_options()) + + 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.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 + f1.assert_called_once_with(id=package_id) + + f2 = self.client['Product_Package'].getConfiguration + f2.assert_called_once_with(id=package_id, mask='mask[itemCategory]') + + f3 = self.client['Product_Package'].getItems + f3.assert_called_once_with(id=package_id, mask='mask[itemCategory]') + + def test_generate_create_dict_with_all_options(self): + package_id = 50 + + self.client['Product_Package'].getAllObjects.return_value = [ + {'name': 'Bare Metal Instance', 'id': package_id}] + + args = { + 'server_core': 100, + 'hourly': False, + 'hostname': 'unicorn', + 'domain': 'giggles.woo', + 'disk0': 500, + 'location': 'Wyrmshire', + 'os': 200, + 'image_id': None, + 'pri_ip_addresses': 300, + 'bandwidth': 400, + 'userdata': None, + 'monitoring': 500, + 'port_speed': 600, + 'vulnerability_scanner': 700, + 'response': 800, + 'vpn_management': 900, + 'remote_management': 1000, + 'notification': 1100, + 'bare_metal': True, + 'database': 1200, + } + + assert_data = { + 'hardware': [{ + 'bareMetalInstanceFlag': args['bare_metal'], + 'hostname': args['hostname'], + 'domain': args['domain'], + }], + 'location': args['location'], + 'packageId': package_id, + 'prices': [ + {'id': args['server_core']}, + {'id': args['disk0']}, + {'id': args['os']}, + {'id': args['pri_ip_addresses']}, + {'id': args['bandwidth']}, + {'id': args['monitoring']}, + {'id': args['port_speed']}, + {'id': args['vulnerability_scanner']}, + {'id': args['response']}, + {'id': args['vpn_management']}, + {'id': args['remote_management']}, + {'id': args['notification']}, + ], + } + + data = self.hardware._generate_create_dict(**args) + + self.assertEqual(data, assert_data) + + @patch('SoftLayer.managers.hardware.HardwareManager._generate_create_dict') + def test_verify_order(self, create_dict): + create_dict.return_value = {'test': 1, 'verify': 1} + self.hardware.verify_order(test=1, verify=1) + create_dict.assert_called_once_with(test=1, verify=1) + f = self.client['Product_Order'].verifyOrder + f.assert_called_once_with({'test': 1, 'verify': 1}) + + @patch('SoftLayer.managers.hardware.HardwareManager._generate_create_dict') + def test_place_order(self, create_dict): + create_dict.return_value = {'test': 1, 'verify': 1} + self.hardware.place_order(test=1, verify=1) + create_dict.assert_called_once_with(test=1, verify=1) + f = self.client['Product_Order'].placeOrder + f.assert_called_once_with({'test': 1, 'verify': 1}) + + def test_cancel_metal_immediately(self): + b_id = 5678 + self.client.__getitem__().getObject.return_value = {'id': '1234', + 'billingItem': { + 'id': b_id, + }} + self.hardware.cancel_metal(b_id, True) + f = self.client['Billing_Item'].cancelService + f.assert_called_once_with(id=b_id) + + def test_cancel_metal_on_anniversary(self): + b_id = 5678 + self.client.__getitem__().getObject.return_value = {'id': '1234', + 'billingItem': { + 'id': b_id, + }} + self.hardware.cancel_metal(b_id, False) + f = self.client['Billing_Item'].cancelServiceOnAnniversaryDate + f.assert_called_once_with(id=b_id) + + def test_cancel_hardware_without_reason(self): + hw_id = 987 + + self.hardware.cancel_hardware(hw_id) + + reasons = self.hardware.get_cancellation_reasons() + + f = self.client['Ticket'].createCancelServerTicket + f.assert_called_once_with(hw_id, reasons['unneeded'], '', True, + 'HARDWARE') + + def test_cancel_hardware_with_reason_and_comment(self): + hw_id = 987 + reason = 'sales' + comment = 'Test Comment' + + self.hardware.cancel_hardware(hw_id, reason, comment) + + reasons = self.hardware.get_cancellation_reasons() + + f = self.client['Ticket'].createCancelServerTicket + f.assert_called_once_with(hw_id, reasons[reason], comment, True, + 'HARDWARE') From ca29cea7e023ba5e03692e24206cd7aea808d827 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 24 Jun 2013 11:21:00 -0500 Subject: [PATCH 0073/3227] Adding missing files and fixing a crash bug after create --- SoftLayer/CLI/core.py | 1 + SoftLayer/CLI/modules/bmetal.py | 541 ++++++++++++++++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 SoftLayer/CLI/modules/bmetal.py diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 63cc7f21d..e795f6975 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -12,6 +12,7 @@ dns Manage DNS firewall Firewall rule and security management hardware View hardware details + bmetal Interact with bare metal instances help Show help iscsi View iSCSI details image Manages compute and flex images diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py new file mode 100644 index 000000000..15be67e95 --- /dev/null +++ b/SoftLayer/CLI/modules/bmetal.py @@ -0,0 +1,541 @@ +""" +usage: sl bmetal [] [...] [options] + sl bmetal [-h | --help] + +Manage bare metal instances + +The available commands are: + create-options Output available available options when creating a server + create Create a new bare metal instance + cancel Cancels a bare metal instance + +For several commands, will be asked for. This can be the id, +hostname or the ip address for a piece of hardware. +""" +import re +from os import linesep +from SoftLayer.CLI import ( + CLIRunnable, Table, no_going_back, confirm, listing, FormattedItem) +from SoftLayer.CLI.helpers import (CLIAbort, SequentialOutput) +from SoftLayer import HardwareManager + + +def resolve_id(manager, identifier): + ids = manager.resolve_ids(identifier) + + if len(ids) == 0: + raise CLIAbort("Error: Unable to find hardware '%s'" % identifier) + + if len(ids) > 1: + raise CLIAbort( + "Error: Multiple hardware found for '%s': %s" % + (identifier, ', '.join([str(_id) for _id in ids]))) + + return ids[0] + + +class BMetalCreateOptions(CLIRunnable): + """ +usage: sl bmetal create-options [options] + +Output available available options when creating a server + +Options: + --all Show all options. default if no other option provided + --datacenter Show datacenter options + --cpu Show CPU options + --nic Show NIC speed options + --disk Show disk options + --os Show operating system options + --memory Show memory size options + --bandwidth Show bandwidth options +""" + action = 'create-options' + options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', 'bandwidth'] + + @classmethod + def execute(cls, client, args): + t = Table(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + + show_all = True + for opt_name in cls.options: + if args.get("--" + opt_name): + show_all = False + break + + mgr = HardwareManager(client) + + bmi_options = mgr.get_bare_metal_create_options() + + if args['--all']: + show_all = True + + if args['--datacenter'] or show_all: + results = cls.get_create_options(bmi_options, 'datacenter')[0] + + t.add_row([results[0], listing(sorted(results[1]))]) + + if args['--cpu'] or args['--memory'] or show_all: + results = cls.get_create_options(bmi_options, '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])))]) + + 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]))]) + + 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], + key=lambda x: int(x[0])))]) + + 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], + key=lambda x: x[0]))]) + + if args['--bandwidth'] or show_all: + results = cls.get_create_options(bmi_options, 'bandwidth')[0] + + t.add_row([results[0], listing( + item[0] for item in sorted(results[1], + key=lambda x: x[0]))]) + + return t + + @classmethod + def get_create_options(cls, bmi_options, section, pretty=True): + """ This method can be used to parse the bare metal instance creation + options into different sections. This can be useful for data validation + as well as printing the options on a help screen. + + :param dict bmi_options: The instance options to parse. Must come from + the .get_bare_metal_create_options() function + in the HardwareManager. + :param string section: The section to parse out. + :param bool pretty: If true, it will return the results in a 'pretty' + format that's easier to print. + """ + if 'datacenter' == section: + datacenters = [loc['keyname'] + for loc in bmi_options['locations']] + return [('datacenter', datacenters)] + elif 'cpu' == section or 'memory' == section: + mem_options = {} + cpu_regex = re.compile('(\d+) x ') + memory_regex = re.compile(' - (\d+) GB Ram', re.I) + + for item in bmi_options['categories']['server_core']['items']: + cpu = cpu_regex.search(item['description']).group(1) + memory = memory_regex.search(item['description']).group(1) + + if cpu and memory: + if memory not in mem_options: + mem_options[memory] = [] + + mem_options[memory].append((cpu, item['price_id'])) + + results = [] + for memory in sorted(mem_options.keys(), key=int): + key = memory + + if pretty: + key = 'cpus (%s gb ram)' % memory + + results.append((key, mem_options[memory])) + + return results + elif 'os' == section: + os_regex = re.compile('(^[A-Za-z\s\/]+) ([\d\.]+)') + bit_regex = re.compile(' \((\d+)\s*bit') + extra_regex = re.compile(' - (.+)\(') + + # Encapsulate the code for generating the operating system code + def _generate_os_code(name, version, bits, extra_info): + name = name.replace(' Linux', '') + name = name.replace('Enterprise', '') + name = name.replace('GNU/Linux', '') + + os_code = name.strip().replace(' ', '_').upper() + + if os_code == 'RED_HAT': + os_code = 'REDHAT' + + if 'UBUNTU' in os_code: + version = re.sub('\.\d+', '', version) + + os_code += '_' + version.replace('.0', '') + + if bits: + os_code += '_' + bits + + if extra_info: + os_code += '_' + extra_info.strip() \ + .replace(' Install', '').upper() + + return os_code + + # Also separate out the code for generating the Windows OS code + # since it's significantly different from the rest. + def _generate_windows_code(description): + version_check = re.search('Windows Server (\d+)', description) + version = version_check.group(1) + + os_code = 'WIN_' + version + + if 'Datacenter' in description: + os_code += '-DC' + elif 'Enterprise' in description: + os_code += '-ENT' + else: + os_code += '-STD' + + if 'ith R2' in description: + os_code += '-R2' + elif 'ith Hyper-V' in description: + os_code += '-HYPERV' + + bit_check = re.search('\((\d+)\s*bit', description) + if bit_check: + os_code += '_' + bit_check.group(1) + + return os_code + + # Loop through the operating systems and get their OS codes + os_list = {} + flat_list = [] + + for os in bmi_options['categories']['os']['items']: + if 'Windows Server' in os['description']: + os_code = _generate_windows_code(os['description']) + else: + os_results = os_regex.search(os['description']) + name = os_results.group(1) + version = os_results.group(2) + bits = bit_regex.search(os['description']) + extra_info = extra_regex.search(os['description']) + + if bits: + bits = bits.group(1) + if extra_info: + extra_info = extra_info.group(1) + + os_code = _generate_os_code(name, version, bits, + extra_info) + + name = os_code.split('_')[0] + + if name not in os_list: + os_list[name] = [] + + os_list[name].append((os_code, os['price_id'])) + flat_list.append((os_code, os['price_id'])) + + if pretty: + results = [] + for os in sorted(os_list.keys()): + results.append(('os (%s)' % os, os_list[os])) + + return results + else: + return [('os', flat_list)] + + elif 'disk' == section: + disks = [] + for disk in bmi_options['categories']['disk0']['items']: + disks.append((disk['capacity'], disk['price_id'])) + + return [('disk(0)', disks)] + elif 'nic' == section: + single = [] + dual = [] + + for item in bmi_options['categories']['port_speed']['items']: + if 'dual' in item['description'].lower(): + dual.append((str(item['capacity']) + '_DUAL', + item['price_id'])) + else: + single.append((item['capacity'], item['price_id'])) + + return [('single nic', single), ('dual nic', dual)] + elif 'bandwidth' == section: + options = [] + for item in bmi_options['categories']['bandwidth']['items']: + if item['capacity']: + options.append((item['capacity'], item['price_id'])) + + return [('bandwidth', options)] + + return [] + + +class CreateBMetalInstance(CLIRunnable): + """ +usage: sl bmetal create --hostname=HOST --domain=DOMAIN --cpu=CPU + --memory=MEMORY --os=OS (--hourly | --monthly) [options] + +Order/create a bare metal instance. See 'sl bmetal create-options' for valid +options + +Required: + -H --hostname=HOST Host portion of the FQDN. example: server + -D --domain=DOMAIN Domain portion of the FQDN example: example.com + -c --cpu=CPU Number of CPU cores + -m --memory=MEMORY Memory in mebibytes (n * 1024) + + NOTE: Due to hardware configurations, the CPU and memory + must match appropriately. See create-options for + options. + + -o OS, --os=OS OS install code. + + --hourly Hourly rate instance type + --monthly Monthly rate instance type + + +Optional: + -d DC, --datacenter=DC datacenter name + Note: Omitting this value defaults to the first + available datacenter + -n MBPS, --network=MBPS Network port speed in Mbps + -b MBPS, --bandwith=MBPS Outbound bandwidth in Mbps + --dry-run, --test Do not create CCI, just get a quote + + --wait=SECONDS Block until CCI is finished provisioning for up to X + seconds before returning. +""" + action = 'create' + options = ['confirm'] + + @classmethod + def execute(cls, client, args): + mgr = HardwareManager(client) + + bmi_options = mgr.get_bare_metal_create_options() + + order = { + 'hostname': args['--hostname'], + 'domain': args['--domain'], + 'bare_metal': True, + } + + # Validate the CPU/Memory combination and get the price ID + server_core = cls._get_cpu_and_memory_price_ids(bmi_options, + args['--cpu'], + args['--memory']) + + if server_core: + order['server_core'] = server_core + else: + raise CLIAbort('Invalid CPU/memory combination specified.') + + order['hourly'] = args['--hourly'] + + # Convert the OS code back into a price ID + os_price = cls._get_price_id_from_options(bmi_options, 'os', + args['--os']) + + if os_price: + order['os'] = os_price + else: + raise CLIAbort('Invalid operating system specified.') + + order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' + + # Set the disk size + if args.get('--disk'): + disk_price = cls._get_price_id_from_options(bmi_options, 'disk', + args.get('--disk')) + else: + disk_price = cls._get_default_value(bmi_options, 'disk0') + + if disk_price: + order['disk0'] = disk_price + else: + raise CLIAbort('Invalid disk size specified.') + + # Set the port speed + port_speed = args.get('--network') or 10 + + nic_price = cls._get_price_id_from_options(bmi_options, 'nic', + port_speed) + + if nic_price: + order['port_speed'] = nic_price + else: + raise CLIAbort('Invalid NIC speed specified.') + + # Get the bandwidth limit and convert it to a price ID. + # Yes, these should be multiplied by 1000, not 1024. + if not args.get('--bandwidth'): + bw_price = cls._get_default_value(bmi_options, 'bandwidth') + else: + try: + bandwidth = int(args.get('--bandwidth', 0)) + if bandwidth < 1000: + bandwidth = bandwidth * 1000 + except ValueError: + unit = args['--bandwidth'][-1] + bandwidth = int(args['--bandwidth'][0:-1]) + if unit in ['G', 'g']: + bandwidth = bandwidth * 1000 + + bw_price = cls._get_price_id_from_options(bmi_options, 'bandwidth', + bandwidth) + + if bw_price: + order['bandwidth'] = bw_price + else: + raise CLIAbort('Invalid bandwidth cap specified.') + + # Now add in the other required values that the user did not specify. + order['pri_ip_addresses'] = cls._get_default_value(bmi_options, + 'pri_ip_addresses') + + order['monitoring'] = cls._get_default_value(bmi_options, 'monitoring') + vuln_scanner = cls._get_default_value(bmi_options, + 'vulnerability_scanner') + order['vulnerability_scanner'] = vuln_scanner + order['response'] = cls._get_default_value(bmi_options, 'response') + order['vpn_management'] = cls._get_default_value(bmi_options, + 'vpn_management') + remote_mgmt = cls._get_default_value(bmi_options, 'remote_management') + order['remote_management'] = remote_mgmt + order['notification'] = cls._get_default_value(bmi_options, + 'notification') + + # Begin output + t = Table(['Item', 'cost']) + t.align['Item'] = 'r' + t.align['cost'] = 'r' + + if args.get('--test'): + result = mgr.verify_order(**order) + + total_monthly = 0.0 + total_hourly = 0.0 + for price in result['prices']: + total_monthly += float(price.get('recurringFee', 0.0)) + total_hourly += float(price.get('hourlyRecurringFee', 0.0)) + if args.get('--hourly'): + rate = "%.2f" % float(price['hourlyRecurringFee']) + else: + rate = "%.2f" % float(price['recurringFee']) + + t.add_row([price['item']['description'], rate]) + + if args.get('--hourly'): + total = total_hourly + else: + total = total_monthly + + billing_rate = 'monthly' + if args.get('--hourly'): + billing_rate = 'hourly' + t.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) + output = SequentialOutput(blanks=False) + output.append(t) + output.append(FormattedItem( + '', + ' -- ! Prices reflected here are retail and do not ' + 'take account level discounts and are not guarenteed.') + ) + elif args['--really'] or confirm( + "This action will incur charges on your account. Continue?"): + result = mgr.place_order(**order) + + print result + t = Table(['name', 'value']) + t.align['name'] = 'r' + t.align['value'] = 'l' + t.add_row(['id', result['orderId']]) + t.add_row(['created', result['orderDate']]) + output = t + else: + raise CLIAbort('Aborting bare metal instance order.') + + return output + + @classmethod + def _get_cpu_and_memory_price_ids(cls, bmi_options, cpu_value, + memory_value): + bmi_obj = BMetalCreateOptions() + price_id = None + + cpu_regex = re.compile('(\d+)') + for k, v in bmi_obj.get_create_options(bmi_options, 'cpu'): + cpu = cpu_regex.search(k).group(1) + + if cpu == cpu_value: + for mem_options in v: + if mem_options[0] == memory_value: + price_id = mem_options[1] + + return price_id + + @classmethod + def _get_default_value(cls, bmi_options, option): + if bmi_options['categories'].get(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)), + ]): + return item['price_id'] + + return None + + @classmethod + def _get_price_id_from_options(cls, bmi_options, option, value): + bmi_obj = BMetalCreateOptions() + price_id = None + + for k, v in bmi_obj.get_create_options(bmi_options, option, False): + for item_options in v: + if item_options[0] == value: + price_id = item_options[1] + + return price_id + + +class CancelInstance(CLIRunnable): + """ +usage: sl bmetal cancel [options] + +Cancel a bare metal instance + +Options: + --immediate Cancels the instance immediately (instead of on the billing + anniversary). +""" + + action = 'cancel' + options = ['confirm'] + + @staticmethod + def execute(client, args): + hw = HardwareManager(client) + hw_id = resolve_id(hw, args.get('')) + + immediate = args.get('--immediate', False) + + if args['--really'] or no_going_back(hw_id): + hw.cancel_metal(hw_id, immediate) + else: + CLIAbort('Aborted') From da2708fc16f914b052d82121c755a831e71201a7 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 25 Jun 2013 14:23:15 -0500 Subject: [PATCH 0074/3227] Fixes a bug where iter was seen as an invalid argument to call() --- SoftLayer/API.py | 2 +- SoftLayer/tests/api_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index aefb660b4..f2024bcd8 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -247,7 +247,7 @@ def call(self, service, method, *args, **kwargs): [...] """ - if kwargs.get('iter'): + if kwargs.pop('iter', False): return self.iter_call(service, method, *args, **kwargs) invalid_kwargs = set(kwargs.keys()) - VALID_CALL_ARGS diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 55eb7f0d4..3a58fe2f8 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -276,7 +276,7 @@ def test_mask_call_invalid_mask(self, make_xml_rpc_api_call): @patch('SoftLayer.API.Client.iter_call') def test_iterate(self, _iter_call): self.client['SERVICE'].METHOD(iter=True) - _iter_call.assert_called_with('SERVICE', 'METHOD', iter=True) + _iter_call.assert_called_with('SERVICE', 'METHOD') @patch('SoftLayer.API.Client.iter_call') def test_service_iter_call(self, _iter_call): From 595ef75f2932abb9825d3b386941d500377e1d83 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 25 Jun 2013 15:10:23 -0500 Subject: [PATCH 0075/3227] Adding some more network stuff to the get_instance() mask (to match the HardwareManager). Also moving port speed changes out of the CLI and into the manager. --- SoftLayer/CLI/modules/cci.py | 7 +++---- SoftLayer/managers/cci.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 7daf7832a..167411aa0 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -641,16 +641,15 @@ def execute(cls, client, args): @staticmethod def exec_port(client, args): - vg = client['Virtual_Guest'] if args['--public']: - func = vg.setPublicNetworkInterfaceSpeed + nic = 'eth1' elif args['--private']: - func = vg.setPrivateNetworkInterfaceSpeed + nic = 'eth0' cci = CCIManager(client) cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') - result = func(args['--speed'], id=cci_id) + result = cci.change_port_speed(cci_id, nic, args['--speed']) if result: return "Success" else: diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index bf01f8932..7a784413c 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -133,6 +133,8 @@ def get_instance(self, id, **kwargs): 'privateNetworkOnlyFlag', 'primaryBackendIpAddress', 'primaryIpAddress', + 'networkComponents[id, status, speed, maxSpeed, name,' + 'macAddress, primaryIpAddress, port, primarySubnet]', 'lastKnownPowerState.name', 'powerState.name', 'maxCpu', @@ -271,6 +273,18 @@ def create_instance(self, **kwargs): create_options = self._generate_create_dict(**kwargs) return self.guest.createObject(create_options) + def change_port_speed(self, id, nic, speed): + if nic not in ['eth0', 'eth1']: + raise ValueError('Can only change speeds on eth0 or eth1') + + if 'eth1' == nic: + func = self.guest.setPublicNetworkInterfaceSpeed + else: + func = self.guest.setPrivateNetworkInterfaceSpeed + + return func(speed, id=id) + + def _get_ids_from_hostname(self, hostname): results = self.list_instances(hostname=hostname, mask="id") return [result['id'] for result in results] From 0c7b848136a43f27d04b36d2623858dd9063ef66 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 25 Jun 2013 16:01:26 -0500 Subject: [PATCH 0076/3227] Adding ability to control NIC speed for hardware (like CCIs) and adding unit tests. --- SoftLayer/CLI/modules/hardware.py | 48 ++++++++++++++++++++++ SoftLayer/managers/hardware.py | 13 +++++- SoftLayer/tests/managers/cci_tests.py | 21 ++++++++++ SoftLayer/tests/managers/hardware_tests.py | 21 ++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 9cab895da..693ff3ace 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -10,6 +10,7 @@ reload Perform an OS reload cancel Cancel a dedicated server. cancel-reasons Provides the list of possible cancellation reasons + network Manage network settings For several commands, will be asked for. This can be the id, hostname or the ip address for a piece of hardware. @@ -238,3 +239,50 @@ def execute(client, args): t.add_row([code, reason]) return t + + +class NetworkHardware(CLIRunnable): + """ +usage: sl hardware network port --speed=SPEED + (--public | --private) [options] + +Manage network settings + +Options: + --speed=SPEED Port speed. 0 disables the port. + [Options: 0, 10, 100, 1000, 10000] + --public Public network + --private Private network +""" + action = 'network' + + @classmethod + 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): + if args['--public']: + nic = 'eth1' + elif args['--private']: + nic = 'eth0' + + mgr = HardwareManager(client) + hw_id = resolve_id(mgr.resolve_ids, args.get(''), + 'hardware') + + result = mgr.change_port_speed(hw_id, nic, args['--speed']) + if result: + return "Success" + else: + return result + + @staticmethod + def exec_detail(client, args): + # TODO this should print out default gateway and stuff + raise CLIAbort('Not implemented') + \ No newline at end of file diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 4be2e9ed8..28873a4ab 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -231,7 +231,7 @@ def get_hardware(self, id, **kwargs): 'primaryBackendIpAddress', 'primaryIpAddress', 'datacenter.name', - 'networkComponents[id, status, maxSpeed, name,' + 'networkComponents[id, status, speed, maxSpeed, name,' 'ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,' 'port, primarySubnet]', 'networkComponents.primarySubnet[id, netmask,' @@ -257,6 +257,17 @@ def reload(self, id): return self.hardware.reloadCurrentOperatingSystemConfiguration( 'FORCE', id=id) + def change_port_speed(self, id, nic, speed): + if nic not in ['eth0', 'eth1']: + raise ValueError('Can only change speeds on eth0 or eth1') + + if 'eth1' == nic: + func = self.hardware.setPublicNetworkInterfaceSpeed + else: + func = self.hardware.setPrivateNetworkInterfaceSpeed + + return func(speed, id=id) + def place_order(self, **kwargs): create_options = self._generate_create_dict(**kwargs) return self.client['Product_Order'].placeOrder(create_options) diff --git a/SoftLayer/tests/managers/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py index 72d9dd838..e754e869a 100644 --- a/SoftLayer/tests/managers/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -492,3 +492,24 @@ def test_wait(self, _sleep): _sleep.assert_has_calls([ call(10), call(10), call(10), call(10), call(10), call(10), call(10), call(10), call(10), call(10)]) + + def test_change_port_speed_public(self): + cci_id = 1 + speed = 100 + self.cci.change_port_speed(cci_id, 'eth1', speed) + + service = self.client['Virtual_Guest'] + f = service.setPublicNetworkInterfaceSpeed + f.assert_called_once_with(speed, id=cci_id) + + def test_change_port_speed_private(self): + cci_id = 2 + speed = 10 + self.cci.change_port_speed(cci_id, 'eth0', speed) + + service = self.client['Virtual_Guest'] + f = service.setPrivateNetworkInterfaceSpeed + f.assert_called_once_with(speed, id=cci_id) + + def test_change_port_speed_errors_with_invalid_nic(self): + self.assertRaises(ValueError, self.cci.change_port_speed, 3, 'mgmt0', 100) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 93b7a918c..d46120c1d 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -262,3 +262,24 @@ def test_cancel_hardware_with_reason_and_comment(self): f = self.client['Ticket'].createCancelServerTicket f.assert_called_once_with(hw_id, reasons[reason], comment, True, 'HARDWARE') + + def test_change_port_speed_public(self): + hw_id = 1 + speed = 100 + self.hardware.change_port_speed(hw_id, 'eth1', speed) + + service = self.client['Hardware_Server'] + f = service.setPublicNetworkInterfaceSpeed + f.assert_called_once_with(speed, id=hw_id) + + def test_change_port_speed_private(self): + hw_id = 2 + speed = 10 + self.hardware.change_port_speed(hw_id, 'eth0', speed) + + service = self.client['Hardware_Server'] + f = service.setPrivateNetworkInterfaceSpeed + f.assert_called_once_with(speed, id=hw_id) + + def test_change_port_speed_errors_with_invalid_nic(self): + self.assertRaises(ValueError, self.hardware.change_port_speed, 3, 'mgmt0', 100) From 823684915393d441392e9d9fe29d2ef38a45fe0c Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 1 Jul 2013 09:41:55 -0500 Subject: [PATCH 0077/3227] Changing port specification after discussion --- SoftLayer/managers/cci.py | 7 ++----- SoftLayer/managers/hardware.py | 7 ++----- SoftLayer/tests/managers/cci_tests.py | 7 ++----- SoftLayer/tests/managers/hardware_tests.py | 7 ++----- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index 7a784413c..f9f5448dc 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -273,11 +273,8 @@ def create_instance(self, **kwargs): create_options = self._generate_create_dict(**kwargs) return self.guest.createObject(create_options) - def change_port_speed(self, id, nic, speed): - if nic not in ['eth0', 'eth1']: - raise ValueError('Can only change speeds on eth0 or eth1') - - if 'eth1' == nic: + def change_port_speed(self, id, public, speed): + if public: func = self.guest.setPublicNetworkInterfaceSpeed else: func = self.guest.setPrivateNetworkInterfaceSpeed diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 28873a4ab..fe1a9a3fa 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -257,11 +257,8 @@ def reload(self, id): return self.hardware.reloadCurrentOperatingSystemConfiguration( 'FORCE', id=id) - def change_port_speed(self, id, nic, speed): - if nic not in ['eth0', 'eth1']: - raise ValueError('Can only change speeds on eth0 or eth1') - - if 'eth1' == nic: + def change_port_speed(self, id, public, speed): + if public: func = self.hardware.setPublicNetworkInterfaceSpeed else: func = self.hardware.setPrivateNetworkInterfaceSpeed diff --git a/SoftLayer/tests/managers/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py index e754e869a..a71e5a82c 100644 --- a/SoftLayer/tests/managers/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -496,7 +496,7 @@ def test_wait(self, _sleep): def test_change_port_speed_public(self): cci_id = 1 speed = 100 - self.cci.change_port_speed(cci_id, 'eth1', speed) + self.cci.change_port_speed(cci_id, True, speed) service = self.client['Virtual_Guest'] f = service.setPublicNetworkInterfaceSpeed @@ -505,11 +505,8 @@ def test_change_port_speed_public(self): def test_change_port_speed_private(self): cci_id = 2 speed = 10 - self.cci.change_port_speed(cci_id, 'eth0', speed) + self.cci.change_port_speed(cci_id, False, speed) service = self.client['Virtual_Guest'] f = service.setPrivateNetworkInterfaceSpeed f.assert_called_once_with(speed, id=cci_id) - - def test_change_port_speed_errors_with_invalid_nic(self): - self.assertRaises(ValueError, self.cci.change_port_speed, 3, 'mgmt0', 100) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index d46120c1d..e5507f775 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -266,7 +266,7 @@ def test_cancel_hardware_with_reason_and_comment(self): def test_change_port_speed_public(self): hw_id = 1 speed = 100 - self.hardware.change_port_speed(hw_id, 'eth1', speed) + self.hardware.change_port_speed(hw_id, True, speed) service = self.client['Hardware_Server'] f = service.setPublicNetworkInterfaceSpeed @@ -275,11 +275,8 @@ def test_change_port_speed_public(self): def test_change_port_speed_private(self): hw_id = 2 speed = 10 - self.hardware.change_port_speed(hw_id, 'eth0', speed) + self.hardware.change_port_speed(hw_id, False, speed) service = self.client['Hardware_Server'] f = service.setPrivateNetworkInterfaceSpeed f.assert_called_once_with(speed, id=hw_id) - - def test_change_port_speed_errors_with_invalid_nic(self): - self.assertRaises(ValueError, self.hardware.change_port_speed, 3, 'mgmt0', 100) From 4efdf66c2fd536f18680728f7e4ae5e7f9fbd692 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 1 Jul 2013 10:12:13 -0500 Subject: [PATCH 0078/3227] Changing CLI to match manager changes --- SoftLayer/CLI/modules/cci.py | 9 ++++----- SoftLayer/CLI/modules/hardware.py | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 167411aa0..ef0a09844 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -641,15 +641,14 @@ def execute(cls, client, args): @staticmethod def exec_port(client, args): - if args['--public']: - nic = 'eth1' - elif args['--private']: - nic = 'eth0' + public = True + if args['--private']: + public = False cci = CCIManager(client) cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') - result = cci.change_port_speed(cci_id, nic, args['--speed']) + result = cci.change_port_speed(cci_id, public, args['--speed']) if result: return "Success" else: diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 693ff3ace..718d788a3 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -266,16 +266,15 @@ def execute(cls, client, args): @staticmethod def exec_port(client, args): - if args['--public']: - nic = 'eth1' - elif args['--private']: - nic = 'eth0' + public = True + if args['--private']: + public = False mgr = HardwareManager(client) hw_id = resolve_id(mgr.resolve_ids, args.get(''), 'hardware') - result = mgr.change_port_speed(hw_id, nic, args['--speed']) + result = mgr.change_port_speed(hw_id, public, args['--speed']) if result: return "Success" else: From de60c5ca9ab00a1baa934516a24169c1de393c90 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Tue, 2 Jul 2013 16:47:56 -0500 Subject: [PATCH 0079/3227] Adding in manager code for ordering dedicated servers --- SoftLayer/managers/hardware.py | 170 ++++++++++++++------- SoftLayer/tests/managers/hardware_tests.py | 127 ++++++++++++++- 2 files changed, 242 insertions(+), 55 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index fe1a9a3fa..dc2bd556a 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -157,58 +157,44 @@ def get_bare_metal_create_options(self): if not hw_id: return None - package = self.client['Product_Package'] - - results = { - 'categories': {}, - 'locations': [] - } - - # 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=hw_id) - - for loc in object_data: - details = loc['location']['locationPackageDetails'][0] - - results['locations'].append({ - 'delivery_information': details.get('deliveryTimeInformation'), - 'keyname': loc['keyname'], - 'long_name': loc['description'], - }) + return self._parse_package_data(hw_id) - for config in package.getConfiguration(id=hw_id, - mask='mask[itemCategory]'): - code = config['itemCategory']['categoryCode'] - category = { - 'sort': config['sort'], - 'step': config['orderStepId'], - 'is_required': config['isRequired'], - 'name': config['itemCategory']['name'], - 'items': [], - } - - results['categories'][code] = category - - # Now pull in the available package item - for item in package.getItems(id=hw_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'], - # TODO - Deal with multiple prices properly. - 'prices': item['prices'], - 'sort': item['prices'][0]['sort'], - 'price_id': item['prices'][0]['id'], - 'capacity': int(item.get('capacity') or 0), - }) - - return results + 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. + """ + 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] + + package_obj = self.client['Product_Package'] + packages = [] + + for package_id in package_ids: + package = package_obj.getObject(id=package_id, + mask='mask[id, name, description]') + + if (package.get('name')): + packages.append((package['id'], package['name'], + package['description'])) + + return packages + + 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. + """ + return self._parse_package_data(package_id) + def get_hardware(self, id, **kwargs): """ Get details about a hardware device @@ -295,7 +281,9 @@ def _generate_create_dict( userdata=None, monitoring=None, port_speed=None, vulnerability_scanner=None, response=None, vpn_management=None, remote_management=None, - notification=None, bare_metal=True, database=None): + notification=None, bare_metal=True, database=None, + package_id=None, firewall=None, server=None, ram=None, + disk_controller=None, lockbox=None, **kwargs): order = { 'hardware': [{ @@ -310,9 +298,13 @@ def _generate_create_dict( if bare_metal: order['packageId'] = self._get_bare_metal_package_id() + else: + order['packageId'] = package_id if server_core: order['prices'].append({'id': int(server_core)}) + elif server: + order['prices'].append({'id': int(server)}) if disk0: order['prices'].append({'id': int(disk0)}) @@ -347,6 +339,24 @@ def _generate_create_dict( if notification: order['prices'].append({'id': int(notification)}) + if firewall: + order['prices'].append({'id': int(firewall)}) + + if ram: + order['prices'].append({'id': int(ram)}) + + if disk_controller: + order['prices'].append({'id': int(disk_controller)}) + + if lockbox: + order['prices'].append({'id': int(lockbox)}) + + # TODO - This is not a good idea. + if kwargs: + for key, price_id in kwargs.iteritems(): + if price_id: + order['prices'].append({'id': int(price_id)}) + return order def _get_bare_metal_package_id(self): @@ -381,3 +391,61 @@ def _get_ids_from_ip(self, ip): results = self.list_hardware(private_ip=ip, mask="id") if results: return [result['id'] for result in results] + + def _parse_package_data(self, id): + package = self.client['Product_Package'] + + results = { + 'categories': {}, + 'locations': [] + } + + # 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) + + for loc in object_data: + details = loc['location']['locationPackageDetails'][0] + + results['locations'].append({ + 'delivery_information': details.get('deliveryTimeInformation'), + 'keyname': loc['keyname'], + 'long_name': loc['description'], + }) + + mask = 'mask[itemCategory[group]]' + + for config in package.getConfiguration(id=id, mask=mask): + code = config['itemCategory']['categoryCode'] + group = NestedDict(config['itemCategory']) or {} + category = { + 'sort': config['sort'], + 'step': config['orderStepId'], + 'is_required': config['isRequired'], + 'name': config['itemCategory']['name'], + 'group': group['group']['name'], + 'items': [], + } + + 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') or 0), + }) + + return results + \ No newline at end of file diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index e5507f775..a635b8eb1 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -15,7 +15,7 @@ class HardwareTests(unittest.TestCase): - + def setUp(self): self.client = MagicMock() self.hardware = HardwareManager(self.client) @@ -141,12 +141,14 @@ def test_get_bare_metal_create_options(self): f1.assert_called_once_with(id=package_id) f2 = self.client['Product_Package'].getConfiguration - f2.assert_called_once_with(id=package_id, mask='mask[itemCategory]') + 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.assert_called_once_with(id=package_id, + mask='mask[itemCategory]') - def test_generate_create_dict_with_all_options(self): + def test_generate_create_dict_with_all_bare_metal_options(self): package_id = 50 self.client['Product_Package'].getAllObjects.return_value = [ @@ -203,6 +205,68 @@ def test_generate_create_dict_with_all_options(self): self.assertEqual(data, assert_data) + def test_generate_create_dict_with_all_dedicated_server_options(self): + args = { + 'server': 100, + 'hostname': 'unicorn', + 'domain': 'giggles.woo', + 'disk0': 500, + 'location': 'Wyrmshire', + 'os': 200, + 'image_id': None, + 'pri_ip_addresses': 300, + 'bandwidth': 400, + 'userdata': None, + 'monitoring': 500, + 'port_speed': 600, + 'vulnerability_scanner': 700, + 'response': 800, + 'vpn_management': 900, + 'remote_management': 1000, + 'notification': 1100, + 'bare_metal': False, + 'database': 1200, + 'package_id': 13, + 'firewall': 1300, + 'ram': 1400, + 'disk_controller': 1500, + 'lockbox': 1600, + 'other': 9999, + } + + assert_data = { + 'hardware': [{ + 'bareMetalInstanceFlag': args['bare_metal'], + 'hostname': args['hostname'], + 'domain': args['domain'], + }], + 'location': args['location'], + 'packageId': 13, + 'prices': [ + {'id': args['server']}, + {'id': args['disk0']}, + {'id': args['os']}, + {'id': args['pri_ip_addresses']}, + {'id': args['bandwidth']}, + {'id': args['monitoring']}, + {'id': args['port_speed']}, + {'id': args['vulnerability_scanner']}, + {'id': args['response']}, + {'id': args['vpn_management']}, + {'id': args['remote_management']}, + {'id': args['notification']}, + {'id': args['firewall']}, + {'id': args['ram']}, + {'id': args['disk_controller']}, + {'id': args['lockbox']}, + {'id': args['other']}, + ], + } + + data = self.hardware._generate_create_dict(**args) + + self.assertEqual(data, assert_data) + @patch('SoftLayer.managers.hardware.HardwareManager._generate_create_dict') def test_verify_order(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} @@ -280,3 +344,58 @@ def test_change_port_speed_private(self): service = self.client['Hardware_Server'] f = service.setPrivateNetworkInterfaceSpeed f.assert_called_once_with(speed, id=hw_id) + + def test_get_available_dedicated_server_packages(self): + self.hardware.get_available_dedicated_server_packages() + + service = self.client['Product_Package'] + f = service.getObject + f.assert_has_calls([call(id=13, mask='mask[id, name, description]')]) + + 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.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 + f1.assert_called_once_with(id=package_id) + + f2 = self.client['Product_Package'].getConfiguration + 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]') + \ No newline at end of file From 035eab76c4fd7b98c12bd115f56a7137e2e8c7c8 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 3 Jul 2013 09:05:54 -0500 Subject: [PATCH 0080/3227] Updating generate_create_dict() so it doesn't accept any option --- SoftLayer/managers/hardware.py | 36 ++++++++++++++-------- SoftLayer/tests/managers/hardware_tests.py | 6 ++-- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index dc2bd556a..bbadf50b4 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -274,16 +274,28 @@ def get_cancellation_reasons(self): } def _generate_create_dict( - self, server_core=None, hourly=True, - hostname=None, domain=None, disk0=None, - location=None, os=None, image_id=None, - pri_ip_addresses=None, bandwidth=None, - userdata=None, monitoring=None, port_speed=None, - vulnerability_scanner=None, response=None, - vpn_management=None, remote_management=None, - notification=None, bare_metal=True, database=None, - package_id=None, firewall=None, server=None, ram=None, - disk_controller=None, lockbox=None, **kwargs): + self, server=None, server_core=None, hostname=None, domain=None, + location=None, os=None, disk0=None, pri_ip_addresses=None, + bandwidth=None, port_speed=None, vulnerability_scanner=None, + response=None, vpn_management=None, remote_management=None, + notification=None, bare_metal=None, database=None, monitoring=None, + ram=None, package_id=None, firewall=None, disk_controller=None, + lockbox=None, **kwargs): + + known_args = ['intrusion_protection', 'os_addon', 'plesk_billing', + 'control_panel', 'web_analytics', 'disk1', 'premium', + 'disk2', 'disk3', 'response', 'nas', 'cdp_backup', + 'static_ipv6_addresses', 'managed_resource', + 'evault_plugin', 'evault', 'virtuozzo', 'bc_insurance', + 'database', 'sec_ip_addresses', 'pri_ipv6_addresses', + 'av_spyware_protection', 'iscsi', 'monitoring_package', + 'disk8', 'disk9', 'disk4', 'disk5', 'disk6', 'disk7', + 'disk10', 'disk11', 'power_supply', 'disk12', 'disk13', + 'disk16', 'disk17', 'disk14', 'disk15', 'disk34', + 'disk35', 'disk18', 'disk19', 'disk30', 'disk32', + 'disk33', 'disk31', 'disk27', 'disk29', 'disk28', + 'disk23', 'disk22', 'disk21', 'disk20', 'disk26', + 'disk25', 'disk24', 'gpu0', 'gpu1'] order = { 'hardware': [{ @@ -351,10 +363,10 @@ def _generate_create_dict( if lockbox: order['prices'].append({'id': int(lockbox)}) - # TODO - This is not a good idea. + # This is a compromise to prevent a truly massive argument list. if kwargs: for key, price_id in kwargs.iteritems(): - if price_id: + if key in known_args and price_id: order['prices'].append({'id': int(price_id)}) return order diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index a635b8eb1..4844cf3fa 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -15,6 +15,8 @@ class HardwareTests(unittest.TestCase): + + maxDiff = None def setUp(self): self.client = MagicMock() @@ -231,7 +233,7 @@ def test_generate_create_dict_with_all_dedicated_server_options(self): 'ram': 1400, 'disk_controller': 1500, 'lockbox': 1600, - 'other': 9999, + 'nas': 9999, } assert_data = { @@ -259,7 +261,7 @@ def test_generate_create_dict_with_all_dedicated_server_options(self): {'id': args['ram']}, {'id': args['disk_controller']}, {'id': args['lockbox']}, - {'id': args['other']}, + {'id': args['nas']}, ], } From d9c9409030793c459435f18dc3d1e0381f8f758e Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 3 Jul 2013 09:24:31 -0500 Subject: [PATCH 0081/3227] PEP8 cleanup --- SoftLayer/managers/hardware.py | 18 +++++++++--------- SoftLayer/tests/managers/hardware_tests.py | 7 ++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index bbadf50b4..c42b2c277 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -163,8 +163,9 @@ 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. + 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, @@ -180,9 +181,9 @@ def get_available_dedicated_server_packages(self): if (package.get('name')): packages.append((package['id'], package['name'], package['description'])) - + return packages - + 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). @@ -194,7 +195,7 @@ def get_dedicated_server_create_options(self, package_id): reasonable lifetime for performance reasons. """ return self._parse_package_data(package_id) - + def get_hardware(self, id, **kwargs): """ Get details about a hardware device @@ -368,7 +369,7 @@ def _generate_create_dict( for key, price_id in kwargs.iteritems(): if key in known_args and price_id: order['prices'].append({'id': int(price_id)}) - + return order def _get_bare_metal_package_id(self): @@ -426,7 +427,7 @@ def _parse_package_data(self, id): }) mask = 'mask[itemCategory[group]]' - + for config in package.getConfiguration(id=id, mask=mask): code = config['itemCategory']['categoryCode'] group = NestedDict(config['itemCategory']) or {} @@ -455,9 +456,8 @@ def _parse_package_data(self, id): 'sort': item['prices'][0]['sort'], 'price_id': item['prices'][0]['id'], 'recurring_fee': float(item['prices'][0].get('recurringFee', - 0)), + 0)), 'capacity': float(item.get('capacity') or 0), }) return results - \ No newline at end of file diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 4844cf3fa..4b5c7ca3e 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -16,8 +16,6 @@ class HardwareTests(unittest.TestCase): - maxDiff = None - def setUp(self): self.client = MagicMock() self.hardware = HardwareManager(self.client) @@ -268,7 +266,7 @@ def test_generate_create_dict_with_all_dedicated_server_options(self): data = self.hardware._generate_create_dict(**args) self.assertEqual(data, assert_data) - + @patch('SoftLayer.managers.hardware.HardwareManager._generate_create_dict') def test_verify_order(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} @@ -349,7 +347,7 @@ def test_change_port_speed_private(self): def test_get_available_dedicated_server_packages(self): self.hardware.get_available_dedicated_server_packages() - + service = self.client['Product_Package'] f = service.getObject f.assert_has_calls([call(id=13, mask='mask[id, name, description]')]) @@ -400,4 +398,3 @@ def test_get_dedicated_server_options(self): f3 = self.client['Product_Package'].getItems f3.assert_called_once_with(id=package_id, mask='mask[itemCategory]') - \ No newline at end of file From 64ea87404ff62e75966a6c95924d9b4af9aff7ab Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 3 Jul 2013 15:06:41 -0500 Subject: [PATCH 0082/3227] First pass at getting dedicated server ordering working at command line --- SoftLayer/CLI/modules/bmetal.py | 7 +- SoftLayer/CLI/modules/hardware.py | 535 +++++++++++++++++++++++++++++- 2 files changed, 535 insertions(+), 7 deletions(-) diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 15be67e95..f4032d73a 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -38,7 +38,7 @@ class BMetalCreateOptions(CLIRunnable): """ usage: sl bmetal create-options [options] -Output available available options when creating a server +Output available available options when creating a bare metal instance. Options: --all Show all options. default if no other option provided @@ -312,10 +312,7 @@ class CreateBMetalInstance(CLIRunnable): available datacenter -n MBPS, --network=MBPS Network port speed in Mbps -b MBPS, --bandwith=MBPS Outbound bandwidth in Mbps - --dry-run, --test Do not create CCI, just get a quote - - --wait=SECONDS Block until CCI is finished provisioning for up to X - seconds before returning. + --dry-run, --test Do not create the instance, just get a quote """ action = 'create' options = ['confirm'] diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 718d788a3..680183ef0 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -11,13 +11,18 @@ cancel Cancel a dedicated server. cancel-reasons Provides the list of possible cancellation reasons 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 For several commands, will be asked for. This can be the id, hostname or the ip address for a piece of hardware. """ +import re +from os import linesep from SoftLayer.CLI.helpers import ( CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, - gb, no_going_back, resolve_id) + SequentialOutput, gb, no_going_back, resolve_id) from SoftLayer import HardwareManager @@ -284,4 +289,530 @@ def exec_port(client, args): def exec_detail(client, args): # TODO this should print out default gateway and stuff raise CLIAbort('Not implemented') - \ No newline at end of file + + +class ListChassisHardware(CLIRunnable): + """ +usage: sl hardware list-chassis + +Display a list of chassis available for ordering dedicated servers. +""" + action = 'list-chassis' + + @staticmethod + def execute(client, args): + t = Table(['Code', 'Chassis']) + t.align['Code'] = 'r' + t.align['Chassis'] = 'l' + + mgr = HardwareManager(client) + chassis = mgr.get_available_dedicated_server_chassis() + + for chassis in chassis: + t.add_row([chassis[0], chassis[1]]) + + return t + + +class HardwareCreateOptions(CLIRunnable): + """ +usage: sl hardware create-options [options] + +Output available available options when creating a dedicated server with the +specified chassis. + +Options: + --all Show all options. default if no other option provided + --datacenter Show datacenter options + --cpu Show CPU options + --nic Show NIC speed options + --disk Show disk options + --os Show operating system options + --memory Show memory size options + --bandwidth Show bandwidth options + --controller Show disk controller options +""" + + action = 'create-options' + options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', 'bandwidth', + 'controller'] + + @classmethod + def execute(cls, client, args): + mgr = HardwareManager(client) + + t = Table(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + + chassis_id = args.get('') + + ds_options = mgr.get_dedicated_server_create_options(chassis_id) + + show_all = True + for opt_name in cls.options: + if args.get("--" + opt_name): + show_all = False + break + + if args['--all']: + show_all = True + + if args['--datacenter'] or show_all: + results = cls.get_create_options(ds_options, 'datacenter')[0] + + t.add_row([results[0], listing(sorted(results[1]))]) + + if args['--cpu'] or show_all: + results = cls.get_create_options(ds_options, 'cpu') + + for result in results: + t.add_row([result[0], listing( + item[0] for item in sorted(result[1], + key=lambda x: x[0]))]) + + if args['--memory'] or show_all: + results = cls.get_create_options(ds_options, 'memory')[0] + + t.add_row([results[0], listing( + item[0] for item in sorted(results[1], + key=lambda x: int(x[0])))]) + + if args['--os'] or show_all: + 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]))]) + + 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], + key=lambda x: x[0]))]) + + if args['--nic'] or show_all: + results = cls.get_create_options(ds_options, 'nic') + + for result in results: + t.add_row([result[0], listing( + item[0] for item in sorted(result[1], + key=lambda x: x[0]))]) + + if args['--bandwidth'] or show_all: + results = cls.get_create_options(ds_options, 'bandwidth')[0] + + t.add_row([results[0], listing( + item[0] for item in sorted(results[1], + key=lambda x: x[0]))]) + + if args['--controller'] or show_all: + results = cls.get_create_options(ds_options, 'disk_controller')[0] + + t.add_row([results[0], listing( + item[0] for item in sorted(results[1], + key=lambda x: x[0]))]) + + return t + + @classmethod + def get_create_options(cls, ds_options, section, pretty=True): + """ This method can be used to parse the bare metal instance creation + options into different sections. This can be useful for data validation + as well as printing the options on a help screen. + + :param dict ds_options: The instance options to parse. Must come from + the .get_bare_metal_create_options() function + in the HardwareManager. + :param string section: The section to parse out. + :param bool pretty: If true, it will return the results in a 'pretty' + format that's easier to print. + """ + if 'datacenter' == section: + datacenters = [loc['keyname'] + for loc in ds_options['locations']] + 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'])])) + + return results + elif 'memory' == section: + ram = [] + for option in ds_options['categories']['ram']['items']: + ram.append((int(option['capacity']), option['price_id'])) + + return [('memory', ram)] + elif 'os' == section: + os_regex = re.compile('(^[A-Za-z\s\/\-]+) ([\d\.]+)') + bit_regex = re.compile(' \((\d+)\s*bit') + extra_regex = re.compile(' - (.+)\(') + + # Encapsulate the code for generating the operating system code + def _generate_os_code(name, version, bits, extra_info): + name = name.replace(' Linux', '') + name = name.replace('Enterprise', '') + name = name.replace('GNU/Linux', '') + + os_code = name.strip().replace(' ', '_').upper() + + if os_code.startswith('RED_HAT'): + os_code = 'REDHAT' + + if 'UBUNTU' in os_code: + version = re.sub('\.\d+', '', version) + + os_code += '_' + version.replace('.0', '') + + if bits: + os_code += '_' + bits + + if extra_info: + garbage = ['Install', '(32 bit)', '(64 bit)'] + + for g in garbage: + extra_info = extra_info.replace(g, '') + + os_code += '_' + \ + extra_info.strip().replace(' ', '_').upper() + + return os_code + + # Also separate out the code for generating the Windows OS code + # since it's significantly different from the rest. + def _generate_windows_code(description): + version_check = re.search('Windows Server (\d+)', description) + version = version_check.group(1) + + os_code = 'WIN_' + version + + if 'Datacenter' in description: + os_code += '-DC' + elif 'Enterprise' in description: + os_code += '-ENT' + else: + os_code += '-STD' + + if 'ith R2' in description: + os_code += '-R2' + elif 'ith Hyper-V' in description: + os_code += '-HYPERV' + + bit_check = re.search('\((\d+)\s*bit', description) + if bit_check: + os_code += '_' + bit_check.group(1) + + return os_code + + # Loop through the operating systems and get their OS codes + os_list = {} + flat_list = [] + + for os in ds_options['categories']['os']['items']: + if 'Windows Server' in os['description']: + os_code = _generate_windows_code(os['description']) + else: + os_results = os_regex.search(os['description']) + name = os_results.group(1) + version = os_results.group(2) + bits = bit_regex.search(os['description']) + extra_info = extra_regex.search(os['description']) + + if bits: + bits = bits.group(1) + if extra_info: + extra_info = extra_info.group(1) + + os_code = _generate_os_code(name, version, bits, + extra_info) + + name = os_code.split('_')[0] + + if name not in os_list: + os_list[name] = [] + + os_list[name].append((os_code, os['price_id'])) + flat_list.append((os_code, os['price_id'])) + + if pretty: + results = [] + for os in sorted(os_list.keys()): + results.append(('os (%s)' % os, os_list[os])) + + return results + else: + return [('os', flat_list)] + + elif 'disk' == section: + disks = [] + type_regex = re.compile('^[\d\.]+[GT]B\s+(.+)$') + for disk in ds_options['categories']['disk0']['items']: + disk_type = 'SATA' + disk_type = type_regex.match(disk['description']).group(1) + + disk_type = disk_type.replace('RPM', '').strip() + disk_type = disk_type.replace(' ', '_').upper() + + #if 'SCSI' in disk['description']: + # disk_type = 'SCSI' + + disk_type = str(int(disk['capacity'])) + '_' + disk_type + disks.append((disk_type, disk['price_id'])) + + return [('disk(0)', disks)] + elif 'nic' == section: + single = [] + dual = [] + + for item in ds_options['categories']['port_speed']['items']: + if 'dual' in item['description'].lower(): + dual.append((str(int(item['capacity'])) + '_DUAL', + item['price_id'])) + else: + single.append((int(item['capacity']), item['price_id'])) + + return [('single nic', single), ('dual nic', dual)] + elif 'bandwidth' == section: + options = [] + for item in ds_options['categories']['bandwidth']['items']: + if item['capacity']: + options.append((int(item['capacity']), item['price_id'])) + + return [('bandwidth', options)] + elif 'disk_controller' == section: + options = [] + for item in ds_options['categories']['disk_controller']['items']: + text = item['description'].replace(' ', '') + + if 'Non-RAID' == text: + text = 'None' + + options.append((text, item['price_id'])) + + return [('disk_controllers', options)] + + return [] + + +class CreateHardware(CLIRunnable): + """ +usage: sl hardware create [options] + +Order/create a dedicated server. See 'sl hardware list-chassis' and +'sl hardware create-options' for valid options + +Required: + -H --hostname=HOST Host portion of the FQDN. example: server + -D --domain=DOMAIN Domain portion of the FQDN example: example.com + --chassis=CHASSIS The chassis to use for the new server + -c --cpu=CPU Number of CPU cores + -o OS, --os=OS OS install code. + -m --memory=MEMORY Memory in mebibytes (n * 1024) + + NOTE: Due to hardware configurations, the CPU and memory + must match appropriately. See create-options for + options. + + +Optional: + -d DC, --datacenter=DC datacenter name + Note: Omitting this value defaults to the first + available datacenter + -n MBPS, --network=MBPS Network port speed in Mbps + -b MBPS, --bandwith=MBPS Outbound bandwidth in Mbps + -d0, --disk0=SIZE Specify the size of the first disk. Defaults to + the lowest cost. + -d1, --disk1=SIZE Size of the second disk. + -d2, --disk2=SIZE Size of the third disk. + -d3, --disk3=SIZE Size of the fourth disk. + --controller=RAID The RAID configuration for the server. + Defaults to None. + --dry-run, --test Do not create the server, just get a quote +""" + action = 'create' + + @classmethod + def execute(cls, client, args): + mgr = HardwareManager(client) + + ds_options = mgr.get_dedicated_server_create_options(args['--chassis']) + + order = { + 'hostname': args['--hostname'], + 'domain': args['--domain'], + 'bare_metal': False, + 'package_id': args['--chassis'], + } + + # Convert the OS code back into a price ID + os_price = cls._get_price_id_from_options(ds_options, 'os', + args['--os']) + + if os_price: + order['os'] = os_price + else: + raise CLIAbort('Invalid operating system specified.') + + order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' + order['server'] = cls._get_price_id_from_options(ds_options, 'cpu', + args['--cpu']) + order['ram'] = cls._get_price_id_from_options(ds_options, 'memory', + int(args['--memory'])) + # Set the disk sizes + has_disks = False + for key in args.keys(): + if key != 'disk_controller' and key.startswith('--disk'): + disk_price = cls._get_price_id_from_options(ds_options, 'disk', + args.get(key)) + + if disk_price: + order[key.replace('--', '')] = disk_price + has_disks = True + + if not has_disks: + disk_price = cls._get_default_value(ds_options, 'disk0') + + # Set the disk controller price + if args.get('--controller'): + dc_price = cls._get_price_id_from_options(ds_options, + 'disk_controller', + args.get('--controller')) + else: + dc_price = cls._get_price_id_from_options(ds_options, + 'disk_controller', + 'None') + + order['disk_controller'] = dc_price + + # Set the port speed + port_speed = args.get('--network') or 10 + + nic_price = cls._get_price_id_from_options(ds_options, 'nic', + port_speed) + + if nic_price: + order['port_speed'] = nic_price + else: + raise CLIAbort('Invalid NIC speed specified.') + + # Get the bandwidth limit and convert it to a price ID. + # Yes, these should be multiplied by 1000, not 1024. + if not args.get('--bandwidth'): + bw_price = cls._get_default_value(ds_options, 'bandwidth') + else: + try: + bandwidth = int(args.get('--bandwidth', 0)) + if bandwidth < 1000: + bandwidth = bandwidth * 1000 + except ValueError: + unit = args['--bandwidth'][-1] + bandwidth = int(args['--bandwidth'][0:-1]) + if unit in ['G', 'g']: + bandwidth = bandwidth * 1000 + + bw_price = cls._get_price_id_from_options(ds_options, 'bandwidth', + bandwidth) + + if bw_price: + order['bandwidth'] = bw_price + else: + raise CLIAbort('Invalid bandwidth cap specified.') + + # Now add in the other required values that the user did not specify. + # TODO - Does this need to be generic? What if stuff costs money? + order['pri_ip_addresses'] = cls._get_default_value(ds_options, + 'pri_ip_addresses') + + order['monitoring'] = cls._get_default_value(ds_options, 'monitoring') + vuln_scanner = cls._get_default_value(ds_options, + 'vulnerability_scanner') + order['vulnerability_scanner'] = vuln_scanner + order['response'] = cls._get_default_value(ds_options, 'response') + order['vpn_management'] = cls._get_default_value(ds_options, + 'vpn_management') + remote_mgmt = cls._get_default_value(ds_options, 'remote_management') + order['remote_management'] = remote_mgmt + order['notification'] = cls._get_default_value(ds_options, + 'notification') + order['lockbox'] = cls._get_default_value(ds_options, 'lockbox') + + # Begin output + t = Table(['Item', 'cost']) + t.align['Item'] = 'r' + t.align['cost'] = 'r' + + if args.get('--test'): + result = mgr.verify_order(**order) + + 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]) + + billing_rate = 'monthly' + if args.get('--hourly'): + billing_rate = 'hourly' + t.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) + output = SequentialOutput(blanks=False) + output.append(t) + output.append(FormattedItem( + '', + ' -- ! Prices reflected here are retail and do not ' + 'take account level discounts and are not guarenteed.') + ) + elif args['--really'] or confirm( + "This action will incur charges on your account. Continue?"): + result = mgr.place_order(**order) + + t = Table(['name', 'value']) + t.align['name'] = 'r' + t.align['value'] = 'l' + t.add_row(['id', result['orderId']]) + t.add_row(['created', result['orderDate']]) + output = t + else: + raise CLIAbort('Aborting bare metal instance order.') + + return output + + @classmethod + def _get_default_value(cls, ds_options, option): + if ds_options['categories'].get(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)), + ]): + return item['price_id'] + + return None + + @classmethod + def _get_price_id_from_options(cls, ds_options, option, value): + 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 From 560636f530786705ad351427649c223d990a0fdd Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 5 Jul 2013 09:45:04 -0500 Subject: [PATCH 0083/3227] Convert --disk options to be a list --- SoftLayer/CLI/modules/hardware.py | 33 ++++++++++++++----------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 680183ef0..e568eeb86 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -22,7 +22,7 @@ from os import linesep from SoftLayer.CLI.helpers import ( CLIRunnable, Table, FormattedItem, NestedDict, CLIAbort, blank, listing, - SequentialOutput, gb, no_going_back, resolve_id) + SequentialOutput, gb, no_going_back, resolve_id, confirm) from SoftLayer import HardwareManager @@ -569,7 +569,7 @@ def _generate_windows_code(description): disk_type = str(int(disk['capacity'])) + '_' + disk_type disks.append((disk_type, disk['price_id'])) - return [('disk(0)', disks)] + return [('disk', disks)] elif 'nic' == section: single = [] dual = [] @@ -606,7 +606,8 @@ def _generate_windows_code(description): class CreateHardware(CLIRunnable): """ -usage: sl hardware create [options] +usage: sl hardware create --hostname=HOST --domain=DOMAIN --cpu=CPU + --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 @@ -615,7 +616,7 @@ class CreateHardware(CLIRunnable): -H --hostname=HOST Host portion of the FQDN. example: server -D --domain=DOMAIN Domain portion of the FQDN example: example.com --chassis=CHASSIS The chassis to use for the new server - -c --cpu=CPU Number of CPU cores + -c --cpu=CPU CPU model -o OS, --os=OS OS install code. -m --memory=MEMORY Memory in mebibytes (n * 1024) @@ -630,11 +631,6 @@ class CreateHardware(CLIRunnable): available datacenter -n MBPS, --network=MBPS Network port speed in Mbps -b MBPS, --bandwith=MBPS Outbound bandwidth in Mbps - -d0, --disk0=SIZE Specify the size of the first disk. Defaults to - the lowest cost. - -d1, --disk1=SIZE Size of the second disk. - -d2, --disk2=SIZE Size of the third disk. - -d3, --disk3=SIZE Size of the fourth disk. --controller=RAID The RAID configuration for the server. Defaults to None. --dry-run, --test Do not create the server, just get a quote @@ -670,14 +666,15 @@ def execute(cls, client, args): int(args['--memory'])) # Set the disk sizes has_disks = False - for key in args.keys(): - if key != 'disk_controller' and key.startswith('--disk'): - disk_price = cls._get_price_id_from_options(ds_options, 'disk', - args.get(key)) + disk_count = 0 + for disk in args.get('--disk'): + disk_price = cls._get_price_id_from_options(ds_options, 'disk', + disk) - if disk_price: - order[key.replace('--', '')] = disk_price - has_disks = True + if disk_price: + order['disk' + str(disk_count)] = disk_price + disk_count += 1 + has_disks = True if not has_disks: disk_price = cls._get_default_value(ds_options, 'disk0') @@ -775,7 +772,7 @@ def execute(cls, client, args): ' -- ! Prices reflected here are retail and do not ' 'take account level discounts and are not guarenteed.') ) - elif args['--really'] or confirm( + elif args.get('--really') or confirm( "This action will incur charges on your account. Continue?"): result = mgr.place_order(**order) @@ -786,7 +783,7 @@ def execute(cls, client, args): t.add_row(['created', result['orderDate']]) output = t else: - raise CLIAbort('Aborting bare metal instance order.') + raise CLIAbort('Aborting dedicated server order.') return output From a8dd332cbd750d03c5053309205a424447290a8a Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 5 Jul 2013 09:47:34 -0500 Subject: [PATCH 0084/3227] Removing inaccurate instructions --- SoftLayer/CLI/modules/hardware.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index e568eeb86..6a3ca89ea 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -620,10 +620,6 @@ class CreateHardware(CLIRunnable): -o OS, --os=OS OS install code. -m --memory=MEMORY Memory in mebibytes (n * 1024) - NOTE: Due to hardware configurations, the CPU and memory - must match appropriately. See create-options for - options. - Optional: -d DC, --datacenter=DC datacenter name From 9c58d0d73fa31bd016fe9c2ff44abe0347a78f63 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 5 Jul 2013 09:48:26 -0500 Subject: [PATCH 0085/3227] Updating usage info --- SoftLayer/CLI/modules/hardware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 6a3ca89ea..c0bac7edd 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -607,7 +607,7 @@ def _generate_windows_code(description): class CreateHardware(CLIRunnable): """ usage: sl hardware create --hostname=HOST --domain=DOMAIN --cpu=CPU - --memory=MEMORY --os=OS --disk=SIZE... [options] + --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 From bcb002135a76fca719a621dbcc7af055f18d873f Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 5 Jul 2013 11:08:38 -0500 Subject: [PATCH 0086/3227] Add in support for GPU and power supply options for specific server chassis --- SoftLayer/CLI/modules/hardware.py | 47 +++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index c0bac7edd..6f794f780 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -306,7 +306,7 @@ def execute(client, args): t.align['Chassis'] = 'l' mgr = HardwareManager(client) - chassis = mgr.get_available_dedicated_server_chassis() + chassis = mgr.get_available_dedicated_server_packages() for chassis in chassis: t.add_row([chassis[0], chassis[1]]) @@ -329,13 +329,14 @@ class HardwareCreateOptions(CLIRunnable): --disk Show disk options --os Show operating system options --memory Show memory size options + --gpus Show GPU options (if any) --bandwidth Show bandwidth options --controller Show disk controller options """ action = 'create-options' options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', 'bandwidth', - 'controller'] + 'gpus', 'ps', 'controller'] @classmethod def execute(cls, client, args): @@ -400,6 +401,13 @@ def execute(cls, client, args): item[0] for item in sorted(result[1], key=lambda x: x[0]))]) + if (args['--gpus'] or show_all) \ + and ds_options['categories'].get('gpu0'): + results = cls.get_create_options(ds_options, 'gpus') + + for result in results: + t.add_row([result[0], listing(item[0] for item in result[1])]) + if args['--bandwidth'] or show_all: results = cls.get_create_options(ds_options, 'bandwidth')[0] @@ -436,7 +444,7 @@ def get_create_options(cls, ds_options, section, pretty=True): elif 'cpu' == section: results = [] cpu_regex = re.compile('\s(\w+)\s(\d+)\s+\-\s+([\d\.]+GHz)' - '\s+\(\w+\)\s+\-\s+(.+)$') + '\s+\([\w ]+\)\s+\-\s+(.+)$') for item in ds_options['categories']['server']['items']: cpu = cpu_regex.search(item['description']) @@ -562,10 +570,6 @@ def _generate_windows_code(description): disk_type = disk_type.replace('RPM', '').strip() disk_type = disk_type.replace(' ', '_').upper() - - #if 'SCSI' in disk['description']: - # disk_type = 'SCSI' - disk_type = str(int(disk['capacity'])) + '_' + disk_type disks.append((disk_type, disk['price_id'])) @@ -582,6 +586,14 @@ def _generate_windows_code(description): single.append((int(item['capacity']), item['price_id'])) return [('single nic', single), ('dual nic', dual)] + elif 'gpus' == section: + results = [] + + for item in ds_options['categories']['gpu0']['items']: + text = 'gpu: ' + item['description'] + results.append((text, [(str(item['id']), item['price_id'])])) + + return results elif 'bandwidth' == section: options = [] for item in ds_options['categories']['bandwidth']['items']: @@ -626,7 +638,9 @@ class CreateHardware(CLIRunnable): Note: Omitting this value defaults to the first available datacenter -n MBPS, --network=MBPS Network port speed in Mbps - -b MBPS, --bandwith=MBPS Outbound bandwidth in Mbps + -b MBPS, --bandwidth=MBPS Outbound bandwidth in Mbps + --gpu0=GPU First GPU option for chassis that support them. + --gpu1=GPU Second GPU option for chassis that support them. --controller=RAID The RAID configuration for the server. Defaults to None. --dry-run, --test Do not create the server, just get a quote @@ -687,6 +701,16 @@ def execute(cls, client, args): order['disk_controller'] = dc_price + if args.get('--gpu0'): + gpu0_price = cls._get_price_id_from_options(ds_options, 'gpus', + args.get('--gpu0')) + order['gpu0'] = gpu0_price + + if args.get('--gpu1'): + gpu1_price = cls._get_price_id_from_options(ds_options, 'gpus', + args.get('--gpu1')) + order['gpu1'] = gpu1_price + # Set the port speed port_speed = args.get('--network') or 10 @@ -739,6 +763,13 @@ def execute(cls, client, args): 'notification') order['lockbox'] = cls._get_default_value(ds_options, 'lockbox') + # Add in additional required values for this chassis. + ps = ds_options['categories'].get('power_supply') + + if ps and ps['is_required']: + order['power_supply'] = cls._get_default_value(ds_options, + 'power_supply') + # Begin output t = Table(['Item', 'cost']) t.align['Item'] = 'r' From 94d9784ef9489a5447e55d7c7565eae77a634bd6 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 5 Jul 2013 11:22:40 -0500 Subject: [PATCH 0087/3227] PEP8 fixes --- SoftLayer/CLI/modules/hardware.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 6f794f780..3b5b23521 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -401,8 +401,8 @@ def execute(cls, client, args): item[0] for item in sorted(result[1], key=lambda x: x[0]))]) - if (args['--gpus'] or show_all) \ - and ds_options['categories'].get('gpu0'): + gpu_categories = ds_options['categories'].get('gpu0') + if (args['--gpus'] or show_all) and gpu_categories: results = cls.get_create_options(ds_options, 'gpus') for result in results: @@ -710,7 +710,7 @@ def execute(cls, client, args): gpu1_price = cls._get_price_id_from_options(ds_options, 'gpus', args.get('--gpu1')) order['gpu1'] = gpu1_price - + # Set the port speed port_speed = args.get('--network') or 10 @@ -769,7 +769,7 @@ def execute(cls, client, args): if ps and ps['is_required']: order['power_supply'] = cls._get_default_value(ds_options, 'power_supply') - + # Begin output t = Table(['Item', 'cost']) t.align['Item'] = 'r' From dbb70b73d0df513f1f3fc9acc1e11500e08532d0 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 8 Jul 2013 10:42:59 -0500 Subject: [PATCH 0088/3227] Changed per peer review notes and future API plans. Cutting down the number of options available when ordering any kind of server. --- SoftLayer/CLI/modules/bmetal.py | 81 ++++------------------- SoftLayer/CLI/modules/hardware.py | 104 ++---------------------------- SoftLayer/managers/hardware.py | 101 +++++++++++------------------ 3 files changed, 55 insertions(+), 231 deletions(-) diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index f4032d73a..424daab31 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -48,10 +48,9 @@ class BMetalCreateOptions(CLIRunnable): --disk Show disk options --os Show operating system options --memory Show memory size options - --bandwidth Show bandwidth options """ action = 'create-options' - options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', 'bandwidth'] + options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic'] @classmethod def execute(cls, client, args): @@ -107,13 +106,6 @@ def execute(cls, client, args): item[0] for item in sorted(result[1], key=lambda x: x[0]))]) - if args['--bandwidth'] or show_all: - results = cls.get_create_options(bmi_options, 'bandwidth')[0] - - t.add_row([results[0], listing( - item[0] for item in sorted(results[1], - key=lambda x: x[0]))]) - return t @classmethod @@ -256,9 +248,9 @@ def _generate_windows_code(description): elif 'disk' == section: disks = [] for disk in bmi_options['categories']['disk0']['items']: - disks.append((disk['capacity'], disk['price_id'])) + disks.append((int(disk['capacity']), disk['price_id'])) - return [('disk(0)', disks)] + return [('disks', disks)] elif 'nic' == section: single = [] dual = [] @@ -271,20 +263,13 @@ def _generate_windows_code(description): single.append((item['capacity'], item['price_id'])) return [('single nic', single), ('dual nic', dual)] - elif 'bandwidth' == section: - options = [] - for item in bmi_options['categories']['bandwidth']['items']: - if item['capacity']: - options.append((item['capacity'], item['price_id'])) - - return [('bandwidth', options)] return [] class CreateBMetalInstance(CLIRunnable): """ -usage: sl bmetal create --hostname=HOST --domain=DOMAIN --cpu=CPU +usage: sl bmetal create --hostname=HOST --domain=DOMAIN --cpu=CPU --disk=DISK... --memory=MEMORY --os=OS (--hourly | --monthly) [options] Order/create a bare metal instance. See 'sl bmetal create-options' for valid @@ -311,7 +296,6 @@ class CreateBMetalInstance(CLIRunnable): Note: Omitting this value defaults to the first available datacenter -n MBPS, --network=MBPS Network port speed in Mbps - -b MBPS, --bandwith=MBPS Outbound bandwidth in Mbps --dry-run, --test Do not create the instance, just get a quote """ action = 'create' @@ -335,7 +319,7 @@ def execute(cls, client, args): args['--memory']) if server_core: - order['server_core'] = server_core + order['server'] = server_core else: raise CLIAbort('Invalid CPU/memory combination specified.') @@ -353,16 +337,16 @@ def execute(cls, client, args): order['location'] = args['--datacenter'] or 'FIRST_AVAILABLE' # Set the disk size - if args.get('--disk'): + disk_prices = [] + for disk in args.get('--disk'): disk_price = cls._get_price_id_from_options(bmi_options, 'disk', - args.get('--disk')) - else: - disk_price = cls._get_default_value(bmi_options, 'disk0') + disk) - if disk_price: - order['disk0'] = disk_price - else: - raise CLIAbort('Invalid disk size specified.') + if disk_price: + disk_prices.append(disk_price) + + if not disk_prices: + disk_price = cls._get_default_value(bmi_options, 'disk0') # Set the port speed port_speed = args.get('--network') or 10 @@ -375,45 +359,6 @@ def execute(cls, client, args): else: raise CLIAbort('Invalid NIC speed specified.') - # Get the bandwidth limit and convert it to a price ID. - # Yes, these should be multiplied by 1000, not 1024. - if not args.get('--bandwidth'): - bw_price = cls._get_default_value(bmi_options, 'bandwidth') - else: - try: - bandwidth = int(args.get('--bandwidth', 0)) - if bandwidth < 1000: - bandwidth = bandwidth * 1000 - except ValueError: - unit = args['--bandwidth'][-1] - bandwidth = int(args['--bandwidth'][0:-1]) - if unit in ['G', 'g']: - bandwidth = bandwidth * 1000 - - bw_price = cls._get_price_id_from_options(bmi_options, 'bandwidth', - bandwidth) - - if bw_price: - order['bandwidth'] = bw_price - else: - raise CLIAbort('Invalid bandwidth cap specified.') - - # Now add in the other required values that the user did not specify. - order['pri_ip_addresses'] = cls._get_default_value(bmi_options, - 'pri_ip_addresses') - - order['monitoring'] = cls._get_default_value(bmi_options, 'monitoring') - vuln_scanner = cls._get_default_value(bmi_options, - 'vulnerability_scanner') - order['vulnerability_scanner'] = vuln_scanner - order['response'] = cls._get_default_value(bmi_options, 'response') - order['vpn_management'] = cls._get_default_value(bmi_options, - 'vpn_management') - remote_mgmt = cls._get_default_value(bmi_options, 'remote_management') - order['remote_management'] = remote_mgmt - order['notification'] = cls._get_default_value(bmi_options, - 'notification') - # Begin output t = Table(['Item', 'cost']) t.align['Item'] = 'r' diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 3b5b23521..d61ccacc6 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -329,14 +329,11 @@ class HardwareCreateOptions(CLIRunnable): --disk Show disk options --os Show operating system options --memory Show memory size options - --gpus Show GPU options (if any) - --bandwidth Show bandwidth options --controller Show disk controller options """ action = 'create-options' - options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', 'bandwidth', - 'gpus', 'ps', 'controller'] + options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', 'controller'] @classmethod def execute(cls, client, args): @@ -401,20 +398,6 @@ def execute(cls, client, args): item[0] for item in sorted(result[1], key=lambda x: x[0]))]) - gpu_categories = ds_options['categories'].get('gpu0') - if (args['--gpus'] or show_all) and gpu_categories: - results = cls.get_create_options(ds_options, 'gpus') - - for result in results: - t.add_row([result[0], listing(item[0] for item in result[1])]) - - if args['--bandwidth'] or show_all: - results = cls.get_create_options(ds_options, 'bandwidth')[0] - - t.add_row([results[0], listing( - item[0] for item in sorted(results[1], - key=lambda x: x[0]))]) - if args['--controller'] or show_all: results = cls.get_create_options(ds_options, 'disk_controller')[0] @@ -586,21 +569,6 @@ def _generate_windows_code(description): single.append((int(item['capacity']), item['price_id'])) return [('single nic', single), ('dual nic', dual)] - elif 'gpus' == section: - results = [] - - for item in ds_options['categories']['gpu0']['items']: - text = 'gpu: ' + item['description'] - results.append((text, [(str(item['id']), item['price_id'])])) - - return results - elif 'bandwidth' == section: - options = [] - for item in ds_options['categories']['bandwidth']['items']: - if item['capacity']: - options.append((int(item['capacity']), item['price_id'])) - - return [('bandwidth', options)] elif 'disk_controller' == section: options = [] for item in ds_options['categories']['disk_controller']['items']: @@ -638,9 +606,6 @@ class CreateHardware(CLIRunnable): Note: Omitting this value defaults to the first available datacenter -n MBPS, --network=MBPS Network port speed in Mbps - -b MBPS, --bandwidth=MBPS Outbound bandwidth in Mbps - --gpu0=GPU First GPU option for chassis that support them. - --gpu1=GPU Second GPU option for chassis that support them. --controller=RAID The RAID configuration for the server. Defaults to None. --dry-run, --test Do not create the server, just get a quote @@ -675,18 +640,15 @@ def execute(cls, client, args): order['ram'] = cls._get_price_id_from_options(ds_options, 'memory', int(args['--memory'])) # Set the disk sizes - has_disks = False - disk_count = 0 + disk_prices = [] for disk in args.get('--disk'): disk_price = cls._get_price_id_from_options(ds_options, 'disk', disk) if disk_price: - order['disk' + str(disk_count)] = disk_price - disk_count += 1 - has_disks = True + disk_prices.append(disk_price) - if not has_disks: + if not disk_prices: disk_price = cls._get_default_value(ds_options, 'disk0') # Set the disk controller price @@ -701,16 +663,6 @@ def execute(cls, client, args): order['disk_controller'] = dc_price - if args.get('--gpu0'): - gpu0_price = cls._get_price_id_from_options(ds_options, 'gpus', - args.get('--gpu0')) - order['gpu0'] = gpu0_price - - if args.get('--gpu1'): - gpu1_price = cls._get_price_id_from_options(ds_options, 'gpus', - args.get('--gpu1')) - order['gpu1'] = gpu1_price - # Set the port speed port_speed = args.get('--network') or 10 @@ -722,54 +674,6 @@ def execute(cls, client, args): else: raise CLIAbort('Invalid NIC speed specified.') - # Get the bandwidth limit and convert it to a price ID. - # Yes, these should be multiplied by 1000, not 1024. - if not args.get('--bandwidth'): - bw_price = cls._get_default_value(ds_options, 'bandwidth') - else: - try: - bandwidth = int(args.get('--bandwidth', 0)) - if bandwidth < 1000: - bandwidth = bandwidth * 1000 - except ValueError: - unit = args['--bandwidth'][-1] - bandwidth = int(args['--bandwidth'][0:-1]) - if unit in ['G', 'g']: - bandwidth = bandwidth * 1000 - - bw_price = cls._get_price_id_from_options(ds_options, 'bandwidth', - bandwidth) - - if bw_price: - order['bandwidth'] = bw_price - else: - raise CLIAbort('Invalid bandwidth cap specified.') - - # Now add in the other required values that the user did not specify. - # TODO - Does this need to be generic? What if stuff costs money? - order['pri_ip_addresses'] = cls._get_default_value(ds_options, - 'pri_ip_addresses') - - order['monitoring'] = cls._get_default_value(ds_options, 'monitoring') - vuln_scanner = cls._get_default_value(ds_options, - 'vulnerability_scanner') - order['vulnerability_scanner'] = vuln_scanner - order['response'] = cls._get_default_value(ds_options, 'response') - order['vpn_management'] = cls._get_default_value(ds_options, - 'vpn_management') - remote_mgmt = cls._get_default_value(ds_options, 'remote_management') - order['remote_management'] = remote_mgmt - order['notification'] = cls._get_default_value(ds_options, - 'notification') - order['lockbox'] = cls._get_default_value(ds_options, 'lockbox') - - # Add in additional required values for this chassis. - ps = ds_options['categories'].get('power_supply') - - if ps and ps['is_required']: - order['power_supply'] = cls._get_default_value(ds_options, - 'power_supply') - # Begin output t = Table(['Item', 'cost']) t.align['Item'] = 'r' diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index c42b2c277..3af8a87e9 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -275,29 +275,14 @@ def get_cancellation_reasons(self): } def _generate_create_dict( - self, server=None, server_core=None, hostname=None, domain=None, - location=None, os=None, disk0=None, pri_ip_addresses=None, - bandwidth=None, port_speed=None, vulnerability_scanner=None, - response=None, vpn_management=None, remote_management=None, - notification=None, bare_metal=None, database=None, monitoring=None, - ram=None, package_id=None, firewall=None, disk_controller=None, - lockbox=None, **kwargs): - - known_args = ['intrusion_protection', 'os_addon', 'plesk_billing', - 'control_panel', 'web_analytics', 'disk1', 'premium', - 'disk2', 'disk3', 'response', 'nas', 'cdp_backup', - 'static_ipv6_addresses', 'managed_resource', - 'evault_plugin', 'evault', 'virtuozzo', 'bc_insurance', - 'database', 'sec_ip_addresses', 'pri_ipv6_addresses', - 'av_spyware_protection', 'iscsi', 'monitoring_package', - 'disk8', 'disk9', 'disk4', 'disk5', 'disk6', 'disk7', - 'disk10', 'disk11', 'power_supply', 'disk12', 'disk13', - 'disk16', 'disk17', 'disk14', 'disk15', 'disk34', - 'disk35', 'disk18', 'disk19', 'disk30', 'disk32', - 'disk33', 'disk31', 'disk27', 'disk29', 'disk28', - 'disk23', 'disk22', 'disk21', 'disk20', 'disk26', - 'disk25', 'disk24', 'gpu0', 'gpu1'] - + self, server=None, hostname=None, domain=None, hourly=False, + location=None, os=None, disks=None, port_speed=None, + bare_metal=None, ram=None, package_id=None, disk_controller=None): + + arguments = ['server', 'hostname', 'domain', 'location', 'os', 'disks', + 'port_speed', 'bare_metal', 'ram', 'package_id', + 'disk_controller', 'server_core'] + order = { 'hardware': [{ 'bareMetalInstanceFlag': bare_metal, @@ -311,64 +296,40 @@ def _generate_create_dict( if bare_metal: order['packageId'] = self._get_bare_metal_package_id() + order['prices'].append({'id': int(server)}) + p_options = self.get_bare_metal_create_options() + if hourly: + order['hourlyBillingFlag'] = True else: order['packageId'] = package_id - - if server_core: - order['prices'].append({'id': int(server_core)}) - elif server: order['prices'].append({'id': int(server)}) + p_options = self.get_dedicated_server_create_options(package_id) - if disk0: - order['prices'].append({'id': int(disk0)}) + if disks: + for disk in disks: + order['prices'].append({'id': int(disk)}) if os: order['prices'].append({'id': int(os)}) - if pri_ip_addresses: - order['prices'].append({'id': int(pri_ip_addresses)}) - - if bandwidth: - order['prices'].append({'id': int(bandwidth)}) - - if monitoring: - order['prices'].append({'id': int(monitoring)}) - if port_speed: order['prices'].append({'id': int(port_speed)}) - if vulnerability_scanner: - order['prices'].append({'id': int(vulnerability_scanner)}) - - if response: - order['prices'].append({'id': int(response)}) - - if vpn_management: - order['prices'].append({'id': int(vpn_management)}) - - if remote_management: - order['prices'].append({'id': int(remote_management)}) - - if notification: - order['prices'].append({'id': int(notification)}) - - if firewall: - order['prices'].append({'id': int(firewall)}) - if ram: order['prices'].append({'id': int(ram)}) if disk_controller: order['prices'].append({'id': int(disk_controller)}) - if lockbox: - order['prices'].append({'id': int(lockbox)}) + # Find all remaining required categories so we can auto-default them + required_fields = [] + for category, data in p_options['categories'].iteritems(): + if data.get('is_required') and category not in arguments: + required_fields.append(category) - # This is a compromise to prevent a truly massive argument list. - if kwargs: - for key, price_id in kwargs.iteritems(): - if key in known_args and price_id: - order['prices'].append({'id': int(price_id)}) + for category in required_fields: + price = self._get_default_value(p_options, category) + order['prices'].append({'id': price}) return order @@ -385,6 +346,20 @@ def _get_bare_metal_package_id(self): return hw_id + def _get_default_value(self, package_options, category): + if package_options['categories'].get(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)), + ]): + return item['price_id'] + + return None + def _get_ids_from_hostname(self, hostname): results = self.list_hardware(hostname=hostname, mask="id") return [result['id'] for result in results] From 5ed1e083bb84b008cd46b0fb5edaca345dd88165 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 8 Jul 2013 11:30:11 -0500 Subject: [PATCH 0089/3227] Forgot to commit unit test updates --- SoftLayer/tests/managers/hardware_tests.py | 56 +++------------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 4b5c7ca3e..6f712ade7 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -155,26 +155,15 @@ def test_generate_create_dict_with_all_bare_metal_options(self): {'name': 'Bare Metal Instance', 'id': package_id}] args = { - 'server_core': 100, + 'server': 100, 'hourly': False, 'hostname': 'unicorn', 'domain': 'giggles.woo', - 'disk0': 500, + 'disks': [500], 'location': 'Wyrmshire', 'os': 200, - 'image_id': None, - 'pri_ip_addresses': 300, - 'bandwidth': 400, - 'userdata': None, - 'monitoring': 500, 'port_speed': 600, - 'vulnerability_scanner': 700, - 'response': 800, - 'vpn_management': 900, - 'remote_management': 1000, - 'notification': 1100, 'bare_metal': True, - 'database': 1200, } assert_data = { @@ -186,18 +175,10 @@ def test_generate_create_dict_with_all_bare_metal_options(self): 'location': args['location'], 'packageId': package_id, 'prices': [ - {'id': args['server_core']}, - {'id': args['disk0']}, + {'id': args['server']}, + {'id': args['disks'][0]}, {'id': args['os']}, - {'id': args['pri_ip_addresses']}, - {'id': args['bandwidth']}, - {'id': args['monitoring']}, {'id': args['port_speed']}, - {'id': args['vulnerability_scanner']}, - {'id': args['response']}, - {'id': args['vpn_management']}, - {'id': args['remote_management']}, - {'id': args['notification']}, ], } @@ -210,28 +191,14 @@ def test_generate_create_dict_with_all_dedicated_server_options(self): 'server': 100, 'hostname': 'unicorn', 'domain': 'giggles.woo', - 'disk0': 500, + 'disks': [500], 'location': 'Wyrmshire', 'os': 200, - 'image_id': None, - 'pri_ip_addresses': 300, - 'bandwidth': 400, - 'userdata': None, - 'monitoring': 500, 'port_speed': 600, - 'vulnerability_scanner': 700, - 'response': 800, - 'vpn_management': 900, - 'remote_management': 1000, - 'notification': 1100, 'bare_metal': False, - 'database': 1200, 'package_id': 13, - 'firewall': 1300, 'ram': 1400, 'disk_controller': 1500, - 'lockbox': 1600, - 'nas': 9999, } assert_data = { @@ -244,22 +211,11 @@ def test_generate_create_dict_with_all_dedicated_server_options(self): 'packageId': 13, 'prices': [ {'id': args['server']}, - {'id': args['disk0']}, + {'id': args['disks'][0]}, {'id': args['os']}, - {'id': args['pri_ip_addresses']}, - {'id': args['bandwidth']}, - {'id': args['monitoring']}, {'id': args['port_speed']}, - {'id': args['vulnerability_scanner']}, - {'id': args['response']}, - {'id': args['vpn_management']}, - {'id': args['remote_management']}, - {'id': args['notification']}, - {'id': args['firewall']}, {'id': args['ram']}, {'id': args['disk_controller']}, - {'id': args['lockbox']}, - {'id': args['nas']}, ], } From 76013446c765ec72223645029497c663d7ea21ba Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 8 Jul 2013 12:08:12 -0500 Subject: [PATCH 0090/3227] Adding missing unit test coverage --- SoftLayer/tests/managers/hardware_tests.py | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 6f712ade7..582d52e19 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -151,9 +151,53 @@ def test_get_bare_metal_create_options(self): 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, + }] + args = { 'server': 100, 'hourly': False, @@ -164,6 +208,7 @@ def test_generate_create_dict_with_all_bare_metal_options(self): 'os': 200, 'port_speed': 600, 'bare_metal': True, + 'hourly': True, } assert_data = { @@ -174,11 +219,13 @@ def test_generate_create_dict_with_all_bare_metal_options(self): }], 'location': args['location'], 'packageId': package_id, + 'hourlyBillingFlag': True, 'prices': [ {'id': args['server']}, {'id': args['disks'][0]}, {'id': args['os']}, {'id': args['port_speed']}, + {'id': prices[0]['price_id']}, ], } From 09e1bedc60c1ab5868de66dad096c5c910079449 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 8 Jul 2013 12:11:09 -0500 Subject: [PATCH 0091/3227] Fixing typo --- SoftLayer/tests/managers/hardware_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 582d52e19..57045db21 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -225,7 +225,7 @@ 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]['price_id']}, + {'id': prices[0]['id']}, ], } From bc9c164300e9c0119105b8baa76be519029cbc40 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 8 Jul 2013 13:35:21 -0500 Subject: [PATCH 0092/3227] Minor fix for external project --- SoftLayer/managers/hardware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 3af8a87e9..e65edca4b 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -281,7 +281,7 @@ def _generate_create_dict( arguments = ['server', 'hostname', 'domain', 'location', 'os', 'disks', 'port_speed', 'bare_metal', 'ram', 'package_id', - 'disk_controller', 'server_core'] + 'disk_controller', 'server_core', 'disk0'] order = { 'hardware': [{ From a4da9dce897d72642e68a88c55199da48b106452 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Mon, 8 Jul 2013 15:10:18 -0500 Subject: [PATCH 0093/3227] Bug fixes from testing the CLI --- SoftLayer/CLI/modules/bmetal.py | 5 +++-- SoftLayer/CLI/modules/hardware.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 424daab31..639796fb9 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -346,7 +346,9 @@ def execute(cls, client, args): disk_prices.append(disk_price) if not disk_prices: - disk_price = cls._get_default_value(bmi_options, 'disk0') + disk_prices.append(cls._get_default_value(bmi_options, 'disk0')) + + order['disks'] = disk_prices # Set the port speed port_speed = args.get('--network') or 10 @@ -399,7 +401,6 @@ def execute(cls, client, args): "This action will incur charges on your account. Continue?"): result = mgr.place_order(**order) - print result t = Table(['name', 'value']) t.align['name'] = 'r' t.align['value'] = 'l' diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index d61ccacc6..f634b085a 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -586,7 +586,7 @@ def _generate_windows_code(description): class CreateHardware(CLIRunnable): """ -usage: sl hardware create --hostname=HOST --domain=DOMAIN --cpu=CPU +usage: sl hardware create --hostname=HOST --domain=DOMAIN --cpu=CPU --chassis=CHASSIS --memory=MEMORY --os=OS --disk=SIZE... [options] Order/create a dedicated server. See 'sl hardware list-chassis' and @@ -649,8 +649,10 @@ def execute(cls, client, args): disk_prices.append(disk_price) if not disk_prices: - disk_price = cls._get_default_value(ds_options, 'disk0') + disk_prices.append(cls._get_default_value(ds_options, 'disk0')) + order['disks'] = disk_prices + # Set the disk controller price if args.get('--controller'): dc_price = cls._get_price_id_from_options(ds_options, From 59fd924afdb9ebc4696fc0d106e1341fac6bd971 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 10 Jul 2013 13:37:27 -0500 Subject: [PATCH 0094/3227] Adding a return value to the authenticate_with_password() function for an external project --- SoftLayer/API.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 3b999af74..bae16b9b4 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -141,6 +141,7 @@ def authenticate_with_password(self, username, password, security_question_id, security_question_answer) self.auth = TokenAuthentication(res['userId'], res['hash']) + return (res['userId'], res['hash']) def add_raw_header(self, name, value): """ Set HTTP headers for API calls. From c04bec7551cd8e31f49d48db163d9bedfa8235f3 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 10 Jul 2013 14:13:56 -0500 Subject: [PATCH 0095/3227] Removing unnecessary lambda options --- SoftLayer/CLI/modules/bmetal.py | 10 ++++------ SoftLayer/CLI/modules/hardware.py | 17 ++++++----------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 639796fb9..8f668f33d 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -95,16 +95,14 @@ def execute(cls, client, args): results = cls.get_create_options(bmi_options, 'disk')[0] t.add_row([results[0], listing( - item[0] for item in sorted(results[1], - key=lambda x: int(x[0])))]) + 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], - key=lambda x: x[0]))]) + item[0] for item in sorted(result[1],))]) return t @@ -257,10 +255,10 @@ def _generate_windows_code(description): for item in bmi_options['categories']['port_speed']['items']: if 'dual' in item['description'].lower(): - dual.append((str(item['capacity']) + '_DUAL', + dual.append((str(int(item['capacity'])) + '_DUAL', item['price_id'])) else: - single.append((item['capacity'], item['price_id'])) + single.append((int(item['capacity']), item['price_id'])) return [('single nic', single), ('dual nic', dual)] diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index f634b085a..1a3b6fc3c 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -364,17 +364,15 @@ def execute(cls, client, args): if args['--cpu'] or show_all: results = cls.get_create_options(ds_options, 'cpu') - for result in results: + for result in sorted(results): t.add_row([result[0], listing( - item[0] for item in sorted(result[1], - key=lambda x: x[0]))]) + item[0] for item in sorted(result[1]))]) if args['--memory'] or show_all: results = cls.get_create_options(ds_options, 'memory')[0] t.add_row([results[0], listing( - item[0] for item in sorted(results[1], - key=lambda x: int(x[0])))]) + item[0] for item in sorted(results[1]))]) if args['--os'] or show_all: results = cls.get_create_options(ds_options, 'os') @@ -387,23 +385,20 @@ def execute(cls, client, args): results = cls.get_create_options(ds_options, 'disk')[0] t.add_row([results[0], linesep.join( - item[0] for item in sorted(results[1], - key=lambda x: x[0]))]) + item[0] for item in sorted(results[1],))]) if args['--nic'] or show_all: results = cls.get_create_options(ds_options, 'nic') for result in results: t.add_row([result[0], listing( - item[0] for item in sorted(result[1], - key=lambda x: x[0]))]) + item[0] for item in sorted(result[1],))]) if args['--controller'] or show_all: results = cls.get_create_options(ds_options, 'disk_controller')[0] t.add_row([results[0], listing( - item[0] for item in sorted(results[1], - key=lambda x: x[0]))]) + item[0] for item in sorted(results[1],))]) return t From 54e441daafaf099fe385b31ea183bafa74e31170 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Wed, 10 Jul 2013 14:28:34 -0500 Subject: [PATCH 0096/3227] Changes based on peer review notes --- SoftLayer/CLI/modules/bmetal.py | 24 ++++++++++++------------ SoftLayer/CLI/modules/hardware.py | 24 ++++++++++++------------ SoftLayer/managers/hardware.py | 26 +++++++++++++------------- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 8f668f33d..05853c0b0 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -429,18 +429,18 @@ def _get_cpu_and_memory_price_ids(cls, bmi_options, cpu_value, @classmethod def _get_default_value(cls, bmi_options, option): - if bmi_options['categories'].get(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)), - ]): - return item['price_id'] - - return None + if option not in bmi_options['categories']: + return + + 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)), + ]): + return item['price_id'] @classmethod def _get_price_id_from_options(cls, bmi_options, option, value): diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 1a3b6fc3c..28297742a 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -717,18 +717,18 @@ def execute(cls, client, args): @classmethod def _get_default_value(cls, ds_options, option): - if ds_options['categories'].get(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)), - ]): - return item['price_id'] - - return None + if option not in ds_options['categories']: + return + + 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)), + ]): + return item['price_id'] @classmethod def _get_price_id_from_options(cls, ds_options, option, value): diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index e65edca4b..3cf91928f 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -347,18 +347,18 @@ def _get_bare_metal_package_id(self): return hw_id def _get_default_value(self, package_options, category): - if package_options['categories'].get(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)), - ]): - return item['price_id'] - - return None + if category not in package_options['categories']: + return + + 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)), + ]): + return item['price_id'] def _get_ids_from_hostname(self, hostname): results = self.list_hardware(hostname=hostname, mask="id") @@ -432,7 +432,7 @@ def _parse_package_data(self, id): 'price_id': item['prices'][0]['id'], 'recurring_fee': float(item['prices'][0].get('recurringFee', 0)), - 'capacity': float(item.get('capacity') or 0), + 'capacity': float(item.get('capacity', 0)), }) return results From fcbaa90c3de3266c81340b5f20e9355d57b34617 Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Thu, 18 Jul 2013 13:03:41 -0500 Subject: [PATCH 0097/3227] CLI: Fixes bug in specifying --userfile --- SoftLayer/CLI/modules/cci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index ef0a09844..92ea643c4 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -404,7 +404,7 @@ def execute(client, args): if args.get('--userdata'): data['userdata'] = args['--userdata'] - elif args.get('userfile'): + elif args.get('--userfile'): f = open(args['--userfile'], 'r') try: data['userdata'] = f.read() From d0dc8eabbe220849943bfaced722fae857791380 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 18 Jul 2013 17:02:23 -0500 Subject: [PATCH 0098/3227] Makes nosetests the test_suite for setup.py This makes `python setup.py test` work. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e9992e716..02e30e307 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ package_data={ 'SoftLayer': ['tests/fixtures/*'], }, + test_suite = 'nose.collector', install_requires=requires, classifiers=[ 'Environment :: Console', From 271953f949a167975676ab05fe0775091ddec373 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 19 Jul 2013 11:36:28 -0500 Subject: [PATCH 0099/3227] Adding in post-install script support when reloading hardware and CCIs. Minor tweaks from code review notes --- SoftLayer/CLI/modules/cci.py | 6 ++- SoftLayer/CLI/modules/hardware.py | 6 ++- SoftLayer/managers/cci.py | 18 ++++++-- SoftLayer/managers/hardware.py | 49 +++++++++++++--------- SoftLayer/tests/managers/cci_tests.py | 8 ++-- SoftLayer/tests/managers/hardware_tests.py | 32 ++++++++++++-- 6 files changed, 87 insertions(+), 32 deletions(-) diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 92ea643c4..78d34f88e 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -501,6 +501,10 @@ class ReloadCCI(CLIRunnable): usage: sl cci reload [options] Reload the OS on a CCI based on its current configuration + +Optional: + -i, --postinstall=URI Post-install script to download + (Only HTTPS executes, HTTP leaves file in /root) """ action = 'reload' @@ -511,7 +515,7 @@ def execute(client, args): cci = CCIManager(client) cci_id = resolve_id(cci.resolve_ids, args.get(''), 'CCI') if args['--really'] or no_going_back(cci_id): - cci.reload_instance(cci_id) + cci.reload_instance(cci_id, args['--postinstall']) else: CLIAbort('Aborted') diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index 28297742a..d3164facd 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -176,6 +176,10 @@ class HardwareReload(CLIRunnable): usage: sl hardware reload [options] Reload the OS on a hardware server based on its current configuration + +Optional: + -i, --postinstall=URI Post-install script to download + (Only HTTPS executes, HTTP leaves file in /root) """ action = 'reload' @@ -187,7 +191,7 @@ def execute(client, args): hardware_id = resolve_id( hardware.resolve_ids, args.get(''), 'hardware') if args['--really'] or no_going_back(hardware_id): - hardware.reload(hardware_id) + hardware.reload(hardware_id, args['--postinstall']) else: CLIAbort('Aborted') diff --git a/SoftLayer/managers/cci.py b/SoftLayer/managers/cci.py index f9f5448dc..54a5b571f 100644 --- a/SoftLayer/managers/cci.py +++ b/SoftLayer/managers/cci.py @@ -165,13 +165,24 @@ def cancel_instance(self, id): """ return self.guest.deleteObject(id=id) - def reload_instance(self, id): + def reload_instance(self, id, post_uri=None): """ Perform an OS reload of an instance with its current configuration. :param integer id: the instance ID to reload + :param string post_url: The URI of the post-install script to run + after reload """ - return self.guest.reloadCurrentOperatingSystemConfiguration(id=id) + payload = { + 'token': 'FORCE', + 'config': {}, + } + + if post_uri: + payload['config']['customProvisionScriptUri'] = post_uri + + return self.guest.reloadOperatingSystem('FORCE', payload['config'], + id=id) def _generate_create_dict( self, cpus=None, memory=None, hourly=True, @@ -280,8 +291,7 @@ def change_port_speed(self, id, public, speed): func = self.guest.setPrivateNetworkInterfaceSpeed return func(speed, id=id) - - + def _get_ids_from_hostname(self, hostname): results = self.list_instances(hostname=hostname, mask="id") return [result['id'] for result in results] diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 3cf91928f..9b92d2d95 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -234,15 +234,25 @@ def get_hardware(self, id, **kwargs): return self.hardware.getObject(id=id, **kwargs) - def reload(self, id): + def reload(self, id, post_uri=None): """ Perform an OS reload of a server with its current configuration. :param integer id: the instance ID to reload + :param string post_url: The URI of the post-install script to run + after reload """ - return self.hardware.reloadCurrentOperatingSystemConfiguration( - 'FORCE', id=id) + payload = { + 'token': 'FORCE', + 'config': {}, + } + + if post_uri: + payload['config']['customProvisionScriptUri'] = post_uri + + return self.hardware.reloadOperatingSystem('FORCE', payload['config'], + id=id) def change_port_speed(self, id, public, speed): if public: @@ -282,7 +292,7 @@ def _generate_create_dict( arguments = ['server', 'hostname', 'domain', 'location', 'os', 'disks', 'port_speed', 'bare_metal', 'ram', 'package_id', 'disk_controller', 'server_core', 'disk0'] - + order = { 'hardware': [{ 'bareMetalInstanceFlag': bare_metal, @@ -328,7 +338,7 @@ def _generate_create_dict( required_fields.append(category) for category in required_fields: - price = self._get_default_value(p_options, category) + price = get_default_value(p_options, category) order['prices'].append({'id': price}) return order @@ -346,20 +356,6 @@ def _get_bare_metal_package_id(self): return hw_id - def _get_default_value(self, package_options, category): - if category not in package_options['categories']: - return - - 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)), - ]): - return item['price_id'] - def _get_ids_from_hostname(self, hostname): results = self.list_hardware(hostname=hostname, mask="id") return [result['id'] for result in results] @@ -436,3 +432,18 @@ def _parse_package_data(self, id): }) return results + + +def get_default_value(package_options, category): + if category not in package_options['categories']: + return + + 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)), + ]): + return item['price_id'] diff --git a/SoftLayer/tests/managers/cci_tests.py b/SoftLayer/tests/managers/cci_tests.py index a71e5a82c..4717c60f6 100644 --- a/SoftLayer/tests/managers/cci_tests.py +++ b/SoftLayer/tests/managers/cci_tests.py @@ -112,10 +112,12 @@ def test_cancel_instance(self): self.client['Virtual_Guest'].deleteObject.assert_called_once_with(id=1) def test_reload_instance(self): - self.cci.reload_instance(id=1) + post_uri = 'http://test.sftlyr.ws/test.sh' + self.cci.reload_instance(id=1, post_uri=post_uri) service = self.client['Virtual_Guest'] - f = service.reloadCurrentOperatingSystemConfiguration - f.assert_called_once_with(id=1) + f = service.reloadOperatingSystem + f.assert_called_once_with('FORCE', + {'customProvisionScriptUri': post_uri}, id=1) @patch('SoftLayer.managers.cci.CCIManager._generate_create_dict') def test_create_verify(self, create_dict): diff --git a/SoftLayer/tests/managers/hardware_tests.py b/SoftLayer/tests/managers/hardware_tests.py index 57045db21..da6d285aa 100644 --- a/SoftLayer/tests/managers/hardware_tests.py +++ b/SoftLayer/tests/managers/hardware_tests.py @@ -6,6 +6,7 @@ :license: BSD, see LICENSE for more details. """ from SoftLayer import HardwareManager +from SoftLayer.managers.hardware import get_default_value try: import unittest2 as unittest @@ -88,9 +89,11 @@ def test_get_hardware(self): id=1, mask=ANY) def test_reload(self): - self.hardware.reload(id=1) - f = self.client.__getitem__().reloadCurrentOperatingSystemConfiguration - f.assert_called_once_with('FORCE', id=1) + post_uri = 'http://test.sftlyr.ws/test.sh' + self.hardware.reload(id=1, post_uri=post_uri) + f = self.client.__getitem__().reloadOperatingSystem + f.assert_called_once_with('FORCE', + {'customProvisionScriptUri': post_uri}, id=1) def test_get_bare_metal_create_options_returns_none_on_error(self): self.client['Product_Package'].getAllObjects.return_value = [ @@ -161,7 +164,7 @@ def test_generate_create_dict_with_all_bare_metal_options(self): 'oneTimeFee': 0, 'laborFee': 0, }] - + self.client['Product_Package'].getAllObjects.return_value = [ {'name': 'Bare Metal Instance', 'id': package_id}] @@ -401,3 +404,24 @@ def test_get_dedicated_server_options(self): f3 = self.client['Product_Package'].getItems f3.assert_called_once_with(id=package_id, mask='mask[itemCategory]') + + def test_get_default_value_returns_none_for_unknown_category(self): + package_options = {'categories': ['Cat1', 'Cat2']} + + self.assertEqual(None, get_default_value(package_options, + 'Unknown Category')) + + def test_get_default_value(self): + price_id = 9876 + package_options = {'categories': + {'Cat1': { + 'items': [{ + 'prices': [{ + 'setupFee': 0, + 'recurringFee': 0, + }], + 'price_id': price_id, + }] + }}} + + self.assertEqual(price_id, get_default_value(package_options, 'Cat1')) From e97fb09593ba2d17b9f8dd7110eb51a4c86a8ed8 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 19 Jul 2013 13:13:22 -0500 Subject: [PATCH 0100/3227] Flake8 Fixes --- SoftLayer/CLI/__init__.py | 4 ++-- SoftLayer/CLI/modules/bmetal.py | 5 +++-- SoftLayer/CLI/modules/cci.py | 3 ++- SoftLayer/CLI/modules/hardware.py | 9 +++++---- SoftLayer/CLI/modules/nas.py | 7 ++++--- SoftLayer/managers/hardware.py | 10 +++++----- SoftLayer/tests/managers/dns_tests.py | 28 +++++++++++++-------------- SoftLayer/tests/transport_tests.py | 6 ++++-- 8 files changed, 39 insertions(+), 33 deletions(-) diff --git a/SoftLayer/CLI/__init__.py b/SoftLayer/CLI/__init__.py index 7d84715c3..7a28c736e 100644 --- a/SoftLayer/CLI/__init__.py +++ b/SoftLayer/CLI/__init__.py @@ -8,5 +8,5 @@ """ -import SoftLayer.CLI.core -from SoftLayer.CLI.helpers import * +import SoftLayer.CLI.core # NOQA +from SoftLayer.CLI.helpers import * # NOQA diff --git a/SoftLayer/CLI/modules/bmetal.py b/SoftLayer/CLI/modules/bmetal.py index 05853c0b0..89b69569b 100644 --- a/SoftLayer/CLI/modules/bmetal.py +++ b/SoftLayer/CLI/modules/bmetal.py @@ -267,8 +267,9 @@ def _generate_windows_code(description): class CreateBMetalInstance(CLIRunnable): """ -usage: sl bmetal create --hostname=HOST --domain=DOMAIN --cpu=CPU --disk=DISK... - --memory=MEMORY --os=OS (--hourly | --monthly) [options] +usage: sl bmetal create --hostname=HOST --domain=DOMAIN --cpu=CPU --os=OS + --memory=MEMORY --disk=DISK... (--hourly | --monthly) + [options] Order/create a bare metal instance. See 'sl bmetal create-options' for valid options diff --git a/SoftLayer/CLI/modules/cci.py b/SoftLayer/CLI/modules/cci.py index 78d34f88e..055cf2d53 100755 --- a/SoftLayer/CLI/modules/cci.py +++ b/SoftLayer/CLI/modules/cci.py @@ -102,7 +102,8 @@ def execute(client, args): mb_to_gb(guest['maxMemory']), guest['primaryIpAddress'] or blank(), guest['primaryBackendIpAddress'] or blank(), - guest['activeTransaction']['transactionStatus'].get('friendlyName') or blank(), + guest['activeTransaction']['transactionStatus'].get( + 'friendlyName') or blank(), ]) return t diff --git a/SoftLayer/CLI/modules/hardware.py b/SoftLayer/CLI/modules/hardware.py index d3164facd..1fbe604f1 100644 --- a/SoftLayer/CLI/modules/hardware.py +++ b/SoftLayer/CLI/modules/hardware.py @@ -337,7 +337,8 @@ class HardwareCreateOptions(CLIRunnable): """ action = 'create-options' - options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', 'controller'] + options = ['datacenter', 'cpu', 'memory', 'os', 'disk', 'nic', + 'controller'] @classmethod def execute(cls, client, args): @@ -585,7 +586,7 @@ def _generate_windows_code(description): class CreateHardware(CLIRunnable): """ -usage: sl hardware create --hostname=HOST --domain=DOMAIN --cpu=CPU +usage: sl hardware create --hostname=HOST --domain=DOMAIN --cpu=CPU --chassis=CHASSIS --memory=MEMORY --os=OS --disk=SIZE... [options] Order/create a dedicated server. See 'sl hardware list-chassis' and @@ -651,7 +652,7 @@ def execute(cls, client, args): disk_prices.append(cls._get_default_value(ds_options, 'disk0')) order['disks'] = disk_prices - + # Set the disk controller price if args.get('--controller'): dc_price = cls._get_price_id_from_options(ds_options, @@ -723,7 +724,7 @@ def execute(cls, client, args): def _get_default_value(cls, ds_options, option): if option not in ds_options['categories']: return - + for item in ds_options['categories'][option]['items']: if not any([ float(item['prices'][0].get('setupFee', 0)), diff --git a/SoftLayer/CLI/modules/nas.py b/SoftLayer/CLI/modules/nas.py index e8faa23f9..674d45a0a 100644 --- a/SoftLayer/CLI/modules/nas.py +++ b/SoftLayer/CLI/modules/nas.py @@ -31,14 +31,15 @@ def execute(client, args): mask='eventCount,serviceResource[datacenter.name]') nas = [NestedDict(n) for n in nas] - t = Table(['id', 'datacenter', 'size', 'username', - 'password', 'server']) + t = Table(['id', 'datacenter', 'size', 'username', 'password', + 'server']) for n in nas: t.add_row([ n['id'], n['serviceResource']['datacenter'].get('name', blank()), - FormattedItem(n.get('capacityGb', blank()), + FormattedItem( + n.get('capacityGb', blank()), "%dGB" % n.get('capacityGb', 0)), n.get('username', blank()), n.get('password', blank()), diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 9b92d2d95..519e0ffc4 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -440,10 +440,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['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)), ]): return item['price_id'] diff --git a/SoftLayer/tests/managers/dns_tests.py b/SoftLayer/tests/managers/dns_tests.py index 202ee81f2..d3fc77b5f 100644 --- a/SoftLayer/tests/managers/dns_tests.py +++ b/SoftLayer/tests/managers/dns_tests.py @@ -66,7 +66,7 @@ def test_resolve_zone_name(self): res = self.dns_client._get_zone_id_from_name('example.com') self.assertEqual([12345], res) self.client['Account'].getDomains.assert_called_once_with( - filter={"domains": {"name": {"operation": "_= example.com"}}}) + filter={"domains": {"name": {"operation": "_= example.com"}}}) # no matches self.client['Account'].getDomains.reset_mock() @@ -74,7 +74,7 @@ def test_resolve_zone_name(self): res = self.dns_client._get_zone_id_from_name('example.com') self.assertEqual([], res) self.client['Account'].getDomains.assert_called_once_with( - filter={"domains": {"name": {"operation": "_= example.com"}}}) + filter={"domains": {"name": {"operation": "_= example.com"}}}) def test_create_zone(self): call = self.client['Dns_Domain'].createObject @@ -150,30 +150,30 @@ def test_get_record(self): D.return_value = [records[1]] self.dns_client.get_records(12345, type='a') D.assert_called_once_with( - id=12345, - filter={'resourceRecords': {'type': {"operation": "_= a"}}}, - mask=ANY) + id=12345, + filter={'resourceRecords': {'type': {"operation": "_= a"}}}, + mask=ANY) D.reset_mock() D.return_value = [records[0]] self.dns_client.get_records(12345, host='a') D.assert_called_once_with( - id=12345, - filter={'resourceRecords': {'host': {"operation": "_= a"}}}, - mask=ANY) + id=12345, + filter={'resourceRecords': {'host': {"operation": "_= a"}}}, + mask=ANY) D.reset_mock() D.return_value = records[3:5] self.dns_client.get_records(12345, data='a') D.assert_called_once_with( - id=12345, - filter={'resourceRecords': {'data': {"operation": "_= a"}}}, - mask=ANY) + id=12345, + filter={'resourceRecords': {'data': {"operation": "_= a"}}}, + mask=ANY) D.reset_mock() D.return_value = records[3:5] self.dns_client.get_records(12345, ttl='86400') D.assert_called_once_with( - id=12345, - filter={'resourceRecords': {'ttl': {"operation": 86400}}}, - mask=ANY) + id=12345, + filter={'resourceRecords': {'ttl': {"operation": 86400}}}, + mask=ANY) diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 19981574e..f64aff440 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -61,7 +61,8 @@ class TestRestAPICall(unittest.TestCase): @patch('SoftLayer.transport.requests.request') def test_json(self, request): request().content = '{}' - resp = make_rest_api_call('GET', 'http://something.com/path/to/resource.json') + resp = make_rest_api_call( + 'GET', 'http://something.com/path/to/resource.json') self.assertEqual(resp, {}) request.assert_called_with( 'GET', 'http://something.com/path/to/resource.json', @@ -87,7 +88,8 @@ def test_json(self, request): @patch('SoftLayer.transport.requests.request') def test_text(self, request): request().text = 'content' - resp = make_rest_api_call('GET', 'http://something.com/path/to/resource.txt') + resp = make_rest_api_call( + 'GET', 'http://something.com/path/to/resource.txt') self.assertEqual(resp, 'content') request.assert_called_with( 'GET', 'http://something.com/path/to/resource.txt', From 3cb1ce6246903ec3831ade20016780d9c2a0c2bd Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 2 Jul 2013 15:09:13 -0500 Subject: [PATCH 0101/3227] Adds Message Queue Service Manager/Connection Class This was a bit of a difficult one because message queue is an altogether different API and don't support all the same ways of authenticating as the core SoftLayer API can and certainly supports username/API key auth differently. The Message Queue CLI module has been refactored to use the new code and is probably a good reference. --- SoftLayer/API.py | 2 + SoftLayer/CLI/modules/messaging.py | 151 ++++++--- SoftLayer/exceptions.py | 4 + SoftLayer/managers/__init__.py | 3 +- SoftLayer/managers/messaging.py | 359 ++++++++++++++++++++ SoftLayer/tests/managers/queue_tests.py | 413 ++++++++++++++++++++++++ docs/api/managers/messaging.rst | 8 + 7 files changed, 892 insertions(+), 48 deletions(-) create mode 100644 SoftLayer/managers/messaging.py create mode 100644 SoftLayer/tests/managers/queue_tests.py create mode 100644 docs/api/managers/messaging.rst diff --git a/SoftLayer/API.py b/SoftLayer/API.py index bae16b9b4..8962b8068 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -268,6 +268,7 @@ def __getitem__(self, name): :param name: The name of the service. E.G. Account Usage: + >>> client = SoftLayer.Client() >>> client['Account'] @@ -286,6 +287,7 @@ def call(self, service, method, *args, **kwargs): :param service: the name of the SoftLayer API service Usage: + >>> client = SoftLayer.Client() >>> client['Account'].getVirtualGuests(mask="id", limit=10) [...] diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py index 76c23fce4..cc1302d18 100644 --- a/SoftLayer/CLI/modules/messaging.py +++ b/SoftLayer/CLI/modules/messaging.py @@ -4,29 +4,31 @@ Manage SoftLayer Message Queue The available commands are: - list-accounts List all queue accounts - queue Queue-related commands - topic Topic-related commands + list-accounts List all queue accounts + list-endpoints List all service endpoints + ping Ping the service + queue Queue-related commands + topic Topic-related commands """ # :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. # :license: BSD, see LICENSE for more details. # from SoftLayer import NetworkManager import sys +from SoftLayer import MessagingManager from SoftLayer.CLI import CLIRunnable, Table -from SoftLayer.CLI.helpers import CLIAbort, listing, ArgumentError +from SoftLayer.CLI.helpers import CLIAbort, listing, ArgumentError, blank -try: - import softlayer_messaging -except ImportError: - raise CLIAbort("""This functionality requires the softlayer_messaging package. -Run 'pip install softlayer_messaging' to install.""") + +COMMON_MESSAGING_ARGS = """Service Options: + --endpoint=NAME Endpoint name + --network=TYPE Network type, [Options: public, private] +""" -def get_mq_client(account_id, env): - client = softlayer_messaging.get_client(account_id) - client.authenticate(env.config.get('username'), env.config.get('api_key')) - return client +def get_mq_client(manager, account_id, env): + return manager.get_connection( + account_id, env.config.get('username'), env.config.get('api_key')) class ListAccounts(CLIRunnable): @@ -40,8 +42,8 @@ class ListAccounts(CLIRunnable): @staticmethod def execute(client, args): - accounts = client['Account'].getMessageQueueAccounts( - mask='id,name,status,nodes') + manager = MessagingManager(client) + accounts = manager.list_accounts() t = Table([ 'id', 'name', 'status' @@ -56,6 +58,53 @@ def execute(client, args): return t +class ListEndpoints(CLIRunnable): + """ +usage: sl messaging list-endpoints [options] + +List SoftLayer Message Queue Endpoints + +""" + action = 'list-endpoints' + + @staticmethod + def execute(client, args): + manager = MessagingManager(client) + regions = manager.get_endpoints() + + t = Table([ + 'name', 'public', 'private' + ]) + for region, endpoints in regions.items(): + t.add_row([ + region, + endpoints.get('public') or blank(), + endpoints.get('private') or blank(), + ]) + + return t + + +class Ping(CLIRunnable): + __doc__ = """ +usage: sl messaging ping [options] + +Ping the SoftLayer Message Queue service + +""" + COMMON_MESSAGING_ARGS + action = 'ping' + + @staticmethod + def execute(client, args): + manager = MessagingManager(client) + ok = manager.ping( + endpoint_name=args['--endpoint'], network=args['--network']) + if ok: + return 'OK' + else: + CLIAbort('Ping failed') + + def queue_table(queue): t = Table(['property', 'value']) t.align['property'] = 'r' @@ -106,7 +155,7 @@ def subscription_table(sub): class Queue(CLIRunnable): - """ + __doc__ = """ usage: sl messaging queue list [options] sl messaging queue detail [options] sl messaging queue create [options] @@ -127,17 +176,20 @@ class Queue(CLIRunnable): --force Flag to force the deletion of the queue even when there are messages Pop Options: - --count=NUM Count of messages to pop -""" + --count=NUM Count of messages to pop + --delete-after Remove popped messages from the queue + +""" + COMMON_MESSAGING_ARGS action = 'queue' @classmethod def execute(cls, client, args): - mq_client = get_mq_client(args[''], cls.env) + manager = MessagingManager(client) + mq_client = get_mq_client(manager, args[''], cls.env) # list if args['list']: - queues = mq_client.queues()['items'] + queues = mq_client.get_queues()['items'] t = Table([ 'name', 'message_count', 'visible_message_count' @@ -151,7 +203,7 @@ def execute(cls, client, args): return t # detail elif args['detail']: - queue = mq_client.queue(args['']).detail() + queue = mq_client.get_queue(args['']) return queue_table(queue) # create elif args['create'] or args['modify']: @@ -169,10 +221,12 @@ def execute(cls, client, args): # delete elif args['delete']: if args['']: - messages = mq_client.queue(args['']).message( - args['']).delete() + messages = mq_client.delete_message( + args[''], + ['']) else: - mq_client.queue(args['']).delete( + mq_client.delete_queue( + args[''], args.get('--force')) # push message elif args['push']: @@ -183,21 +237,28 @@ def execute(cls, client, args): else: body = sys.stdin.read() return message_table( - mq_client.queue(args['']).push(body)) + mq_client.push_queue_message(args[''], body)) # pop message elif args['pop']: - messages = mq_client.queue(args['']).pop( + messages = mq_client.pop_message( + args[''], args.get('--count') or 1) formatted_messages = [] for message in messages['items']: formatted_messages.append(message_table(message)) + + if args.get('--delete-after'): + for message in messages['items']: + mq_client.delete_message( + args[''], + message['id']) return formatted_messages else: raise CLIAbort('Invalid command') class Topic(CLIRunnable): - """ + __doc__ = """ usage: sl messaging topic list [options] sl messaging topic detail [options] sl messaging topic create [options] @@ -217,16 +278,17 @@ class Topic(CLIRunnable): Topic Delete Options: --force Flag to force the deletion of the topic even when there are subscriptions -""" +""" + COMMON_MESSAGING_ARGS action = 'topic' @classmethod def execute(cls, client, args): - mq_client = get_mq_client(args[''], cls.env) + manager = MessagingManager(client) + mq_client = get_mq_client(manager, args[''], cls.env) # list if args['list']: - topics = mq_client.topics()['items'] + topics = mq_client.get_topics()['items'] t = Table(['name']) for topic in topics: @@ -234,8 +296,8 @@ def execute(cls, client, args): return t # detail elif args['detail']: - topic = mq_client.topic(args['']).detail() - subscriptions = mq_client.topic(args['']).subscriptions() + topic = mq_client.get_topic(args['']) + subscriptions = mq_client.get_subscriptions(args['']) tables = [] for sub in subscriptions['items']: tables.append(subscription_table(sub)) @@ -243,14 +305,15 @@ def execute(cls, client, args): # create elif args['create']: if args['--subscription']: - topic = mq_client.topic(args['']) if args['--type'] == 'queue': - subscription = topic.create_subscription( + subscription = mq_client.create_subscription( + args[''], 'queue', queue_name=args['--queue-name'], ) elif args['--type'] == 'http': - subscription = topic.create_subscription( + subscription = mq_client.create_subscription( + args[''], 'http', method=args['--http-method'] or 'GET', url=args['--http-url'], @@ -275,10 +338,12 @@ def execute(cls, client, args): # delete elif args['delete']: if args['']: - messages = mq_client.topic(args['']).subscription( - args['']).delete() + mq_client.delete_subscription( + args[''], + args['']) else: - mq_client.topic(args['']).delete( + mq_client.delete_topic( + args[''], args.get('--force')) # push message elif args['push']: @@ -289,14 +354,6 @@ def execute(cls, client, args): else: body = sys.stdin.read() return message_table( - mq_client.topic(args['']).push(body)) - # pop message - elif args['pop']: - messages = mq_client.topic(args['']).pop( - args.get('--count') or 1) - formatted_messages = [] - for message in messages['items']: - formatted_messages.append(message_table(message)) - return formatted_messages + mq_client.push_topic_message(args[''], body)) else: raise CLIAbort('Invalid command') diff --git a/SoftLayer/exceptions.py b/SoftLayer/exceptions.py index 6488ef0d6..3a24bfe95 100644 --- a/SoftLayer/exceptions.py +++ b/SoftLayer/exceptions.py @@ -12,6 +12,10 @@ class SoftLayerError(StandardError): " The base SoftLayer error. " +class Unauthenticated(StandardError): + " Unauthenticated " + + class SoftLayerAPIError(SoftLayerError): """ SoftLayerAPIError is an exception raised whenever an error is returned from the API. diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index bb821caa6..72278d4f5 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -12,8 +12,9 @@ from SoftLayer.managers.dns import DNSManager from SoftLayer.managers.firewall import FirewallManager from SoftLayer.managers.hardware import HardwareManager +from SoftLayer.managers.messaging import MessagingManager from SoftLayer.managers.metadata import MetadataManager from SoftLayer.managers.ssl import SSLManager __all__ = ['CCIManager', 'DNSManager', 'FirewallManager', 'HardwareManager', - 'MetadataManager', 'SSLManager'] + 'MessagingManager', 'MetadataManager', 'SSLManager'] diff --git a/SoftLayer/managers/messaging.py b/SoftLayer/managers/messaging.py new file mode 100644 index 000000000..d5dbc7b53 --- /dev/null +++ b/SoftLayer/managers/messaging.py @@ -0,0 +1,359 @@ +import json +import requests.auth + +from SoftLayer.consts import USER_AGENT +from SoftLayer.exceptions import Unauthenticated + +ENDPOINTS = { + "dal05": { + "public": "dal05.mq.softlayer.net", + "private": "dal05.mq.service.networklayer.com" + } +} + + +class QueueAuth(requests.auth.AuthBase): + """ Message Queue authentication for requests + + :param endpoint: endpoint URL + :param username: SoftLayer username + :param api_key: SoftLayer API Key + :param auth_token: (optional) Starting auth token + """ + def __init__(self, endpoint, username, api_key, auth_token=None): + self.endpoint = endpoint + self.username = username + self.api_key = api_key + self.auth_token = auth_token + + def auth(self): + """ Do Authentication """ + headers = { + 'X-Auth-User': self.username, + 'X-Auth-Key': self.api_key + } + resp = requests.post(self.endpoint, headers=headers) + if resp.ok: + self.auth_token = resp.headers['X-Auth-Token'] + else: + raise Unauthenticated("Error while authenticating", resp) + + def handle_error(self, r, **kwargs): + """ Handle errors """ + r.request.deregister_hook('response', self.handle_error) + if r.status_code == 503: + r.request.send(anyway=True) + elif r.status_code == 401: + self.auth() + r.request.headers['X-Auth-Token'] = self.auth_token + r.request.send(anyway=True) + + def __call__(self, r): + """ Attach auth token to the request. Do authentication if an auth + token isn't available + """ + if not self.auth_token: + self.auth() + r.register_hook('response', self.handle_error) + r.headers['X-Auth-Token'] = self.auth_token + return r + + +class MessagingManager(object): + """ Manage SoftLayer Message Queue """ + def __init__(self, client): + self.client = client + + def list_accounts(self, **kwargs): + """ List message queue accounts + + :param dict \*\*kwargs: response-level arguments (limit, offset, etc.) + """ + if 'mask' not in kwargs: + items = set([ + 'id', + 'name', + 'status', + 'nodes', + ]) + kwargs['mask'] = "mask[%s]" % ','.join(items) + + return self.client['Account'].getMessageQueueAccounts(**kwargs) + + def get_endpoint(self, datacenter=None, network=None): + """ Get a message queue endpoint based on datacenter/network type + + :param datacenter: datacenter code + :param network: network ('public' or 'private') + """ + if datacenter is None: + datacenter = 'dal05' + if network is None: + network = 'public' + try: + host = ENDPOINTS[datacenter][network] + return "https://%s" % host + except KeyError: + raise TypeError('Invalid endpoint %s/%s' + % (datacenter, network)) + + def get_endpoints(self): + """ Get all known message queue endpoints """ + return ENDPOINTS + + def get_connection(self, id, username, api_key, datacenter=None, + network=None): + """ Get connection to Message Queue Service + + :param id: Message Queue Account id + :param username: SoftLayer username + :param api_key: SoftLayer API key + :param datacenter: Datacenter code + :param network: network ('public' or 'private') + """ + client = MessagingConnection( + id, endpoint=self.get_endpoint(datacenter, network)) + client.authenticate(username, api_key) + return client + + def ping(self, datacenter=None, network=None): + r = requests.get('%s/v1/ping' % + self.get_endpoint(datacenter, network)) + r.raise_for_status() + return True + + +class MessagingConnection(object): + """ Message Queue Service Connection + + :param id: Message Queue Account id + :param endpoint: Endpoint URL + """ + def __init__(self, id, endpoint=None): + self.account_id = id + self.endpoint = endpoint + self.auth = None + + def make_request(self, method, path, **kwargs): + """ Make request. Generally not called directly + + :param method: HTTP Method + :param path: resource Path + :param dict \*\*kwargs: extra request arguments + """ + headers = { + 'Content-Type': 'application/json', + 'User-Agent': USER_AGENT, + } + kwargs['headers'] = dict( + headers.items() + kwargs.get('headers', {}).items()) + kwargs['auth'] = self.auth + + url = '/'.join((self.endpoint, 'v1', self.account_id, path)) + r = requests.request(method, url, **kwargs) + r.raise_for_status() + return r + + def authenticate(self, username, api_key, auth_token=None): + """ Make request. Generally not called directly + + :param username: SoftLayer username + :param api_key: SoftLayer API Key + :param auth_token: (optional) Starting auth token + """ + auth_endpoint = '/'.join((self.endpoint, 'v1', self.account_id, 'auth')) + auth = QueueAuth(auth_endpoint, username, api_key, + auth_token=auth_token) + auth.auth() + self.auth = auth + + def stats(self, period='hour'): + """ Get account stats + + :param period: 'hour', 'day', 'week', 'month' + """ + r = self.make_request('get', 'stats/%s' % period) + return json.loads(r.content) + + # QUEUE METHODS + + def get_queues(self, tags=None): + """ Get listing of queues + + :param list tags: (optional) list of tags to filter by + """ + params = {} + if tags: + params['tags'] = ','.join(tags) + r = self.make_request('get', 'queues', params=params) + return json.loads(r.content) + + def create_queue(self, queue_name, **kwargs): + """ Create Queue + + :param queue_name: Queue Name + :param dict \*\*kwargs: queue options + """ + queue = {} + queue.update(kwargs) + data = json.dumps(queue) + r = self.make_request('put', 'queues/%s' % queue_name, data=data) + return json.loads(r.content) + + def modify_queue(self, queue_name, **kwargs): + """ Modify Queue + + :param queue_name: Queue Name + :param dict \*\*kwargs: queue options + """ + return self.create_queue(queue_name, **kwargs) + + def get_queue(self, queue_name): + """ Get queue details + + :param queue_name: Queue Name + """ + r = self.make_request('get', 'queues/%s' % queue_name) + return json.loads(r.content) + + def delete_queue(self, queue_name, force=False): + """ Delete Queue + + :param queue_name: Queue Name + :param force: (optional) Force queue to be deleted even if there + are pending messages + """ + params = {} + if force: + params['force'] = 1 + self.make_request('delete', 'queues/%s' % queue_name, params=params) + return True + + def push_queue_message(self, queue_name, body, **kwargs): + """ Create Queue Message + + :param queue_name: Queue Name + :param body: Message body + :param dict \*\*kwargs: Message options + """ + message = {'body': body} + message.update(kwargs) + r = self.make_request('post', 'queues/%s/messages' % queue_name, + data=json.dumps(message)) + return json.loads(r.content) + + def pop_message(self, queue_name, count=1): + """ Pop message from a queue + + :param queue_name: Queue Name + :param count: (optional) number of messages to retrieve + """ + r = self.make_request('get', 'queues/%s/messages' % queue_name, + params={'batch': count}) + return json.loads(r.content) + + def delete_message(self, queue_name, message_id): + """ Delete a message + + :param queue_name: Queue Name + :param message_id: Message id + """ + self.make_request('delete', 'queues/%s/messages/%s' + % (queue_name, message_id)) + return True + + # TOPIC METHODS + + def get_topics(self, tags=None): + """ Get listing of topics + + :param list tags: (optional) list of tags to filter by + """ + params = {} + if tags: + params['tags'] = ','.join(tags) + r = self.make_request('get', 'topics', params=params) + return json.loads(r.content) + + def create_topic(self, topic_name, **kwargs): + """ Create Topic + + :param topic_name: Topic Name + :param dict \*\*kwargs: Topic options + """ + data = json.dumps(kwargs) + r = self.make_request('put', 'topics/%s' % topic_name, data=data) + return json.loads(r.content) + + def modify_topic(self, topic_name, **kwargs): + """ Modify Topic + + :param topic_name: Topic Name + :param dict \*\*kwargs: Topic options + """ + return self.create_topic(topic_name, **kwargs) + + def get_topic(self, topic_name): + """ Get topic details + + :param topic_name: Topic Name + """ + r = self.make_request('get', 'topics/%s' % topic_name) + return json.loads(r.content) + + def delete_topic(self, topic_name, force=False): + """ Delete Topic + + :param topic_name: Topic Name + :param force: (optional) Force topic to be deleted even if there + are attached subscribers + """ + params = {} + if force: + params['force'] = 1 + self.make_request('delete', 'topics/%s' % topic_name, params=params) + return True + + def push_topic_message(self, topic_name, body, **kwargs): + """ Create Topic Message + + :param topic_name: Topic Name + :param body: Message body + :param dict \*\*kwargs: Topic message options + """ + message = {'body': body} + message.update(kwargs) + r = self.make_request('post', 'topics/%s/messages' % topic_name, + data=json.dumps(message)) + return json.loads(r.content) + + def get_subscriptions(self, topic_name): + """ Listing of subscriptions on a topic + + :param topic_name: Topic Name + """ + r = self.make_request('get', 'topics/%s/subscriptions' % topic_name) + return json.loads(r.content) + + def create_subscription(self, topic_name, type, **kwargs): + """ Create Subscription + + :param topic_name: Topic Name + :param type: type ('queue' or 'http') + :param dict \*\*kwargs: Subscription options + """ + r = self.make_request( + 'post', 'topics/%s/subscriptions' % topic_name, + data=json.dumps({'endpoint_type': type, 'endpoint': kwargs})) + return json.loads(r.content) + + def delete_subscription(self, topic_name, subscription_id): + """ Delete a subscription + + :param topic_name: Topic Name + :param subscription_id: Subscription id + """ + self.make_request('delete', 'topics/%s/subscriptions/%s' % + (topic_name, subscription_id)) + return True + diff --git a/SoftLayer/tests/managers/queue_tests.py b/SoftLayer/tests/managers/queue_tests.py new file mode 100644 index 000000000..d6a39b8de --- /dev/null +++ b/SoftLayer/tests/managers/queue_tests.py @@ -0,0 +1,413 @@ +""" + SoftLayer.tests.managers.cci_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013, SoftLayer Technologies, Inc. All rights reserved. + :license: BSD, see LICENSE for more details. +""" +from SoftLayer import MessagingManager, Unauthenticated +import SoftLayer.managers.messaging +from SoftLayer.consts import USER_AGENT + +import json +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +from mock import MagicMock, patch, ANY + +QUEUE_1 = { + 'expiration': 40000, + 'message_count': 0, + 'name': 'example_queue', + 'tags': ['tag1', 'tag2', 'tag3'], + 'visibility_interval': 10, + 'visible_message_count': 0} +QUEUE_LIST = {'item_count': 1, 'items': [QUEUE_1]} +MESSAGE_1 = { + 'body': '', + 'fields': {'field': 'value'}, + 'id': 'd344a01133b61181f57d9950a852eb10', + 'initial_entry_time': 1343402631.3917992, + 'message': 'Object created', + 'visibility_delay': 0, + 'visibility_interval': 30000} + +TOPIC_1 = {'name': 'example_topic', 'tags': ['tag1', 'tag2', 'tag3']} +TOPIC_LIST = {'item_count': 1, 'items': [TOPIC_1]} +SUBSCRIPTION_1 = { + 'endpoint': { + 'account_id': 'test', + 'queue_name': 'topic_subscription_queue'}, + 'endpoint_type': 'queue', + 'id': 'd344a01133b61181f57d9950a85704d4', + 'message': 'Object created'} +SUBSCRIPTION_LIST = {'item_count': 1, 'items': [SUBSCRIPTION_1]} + + + +def mocked_auth_call(self): + self.auth_token = 'NEW_AUTH_TOKEN' + + +class QueueAuthTests(unittest.TestCase): + def setUp(self): + self.auth = SoftLayer.managers.messaging.QueueAuth( + 'endpoint', 'username', 'api_key', auth_token='auth_token') + + def test_init(self): + auth = SoftLayer.managers.messaging.QueueAuth( + 'endpoint', 'username', 'api_key', auth_token='auth_token') + self.assertEqual(auth.endpoint, 'endpoint') + self.assertEqual(auth.username, 'username') + self.assertEqual(auth.api_key, 'api_key') + self.assertEqual(auth.auth_token, 'auth_token') + + @patch('SoftLayer.managers.messaging.requests.post') + def test_auth(self, post): + post().headers = {'X-Auth-Token': 'NEW_AUTH_TOKEN'} + post().ok = True + self.auth.auth() + self.auth.auth_token = 'NEW_AUTH_TOKEN' + + post().ok = False + self.assertRaises(Unauthenticated, self.auth.auth) + + @patch('SoftLayer.managers.messaging.QueueAuth.auth', mocked_auth_call) + def test_handle_error_200(self): + # No op on no error + request = MagicMock() + request.status_code = 200 + self.auth.handle_error(request) + + self.assertEqual(self.auth.auth_token, 'auth_token') + self.assertFalse(request.request.send.called) + + @patch('SoftLayer.managers.messaging.QueueAuth.auth', mocked_auth_call) + def test_handle_error_503(self): + # Retry once more on 503 error + request = MagicMock() + request.status_code = 503 + self.auth.handle_error(request) + + self.assertEqual(self.auth.auth_token, 'auth_token') + request.request.send.assert_called_with(anyway=True) + + @patch('SoftLayer.managers.messaging.QueueAuth.auth', mocked_auth_call) + def test_handle_error_401(self): + # Re-auth on 401 + request = MagicMock() + request.status_code = 401 + request.request.headers = {'X-Auth-Token': 'OLD_AUTH_TOKEN'} + self.auth.handle_error(request) + + self.assertEqual(self.auth.auth_token, 'NEW_AUTH_TOKEN') + request.request.send.assert_called_with(anyway=True) + + @patch('SoftLayer.managers.messaging.QueueAuth.auth', mocked_auth_call) + def test_call_unauthed(self): + request = MagicMock() + request.headers = {} + self.auth.auth_token = None + self.auth(request) + + self.assertEqual(self.auth.auth_token, 'NEW_AUTH_TOKEN') + request.register_hook.assert_called_with( + 'response', self.auth.handle_error) + self.assertEqual(request.headers, {'X-Auth-Token': 'NEW_AUTH_TOKEN'}) + + + +class MessagingManagerTests(unittest.TestCase): + + def setUp(self): + self.client = MagicMock() + self.manager = MessagingManager(self.client) + + def test_list_accounts(self): + self.manager.list_accounts() + self.client['Account'].getMessageQueueAccounts.assert_called_with( + mask=ANY) + + def test_get_endpoints(self): + endpoints = self.manager.get_endpoints() + self.assertEqual(endpoints, SoftLayer.managers.messaging.ENDPOINTS) + + @patch('SoftLayer.managers.messaging.ENDPOINTS', { + 'datacenter01': { + 'private': 'private_endpoint', 'public': 'public_endpoint'}, + 'dal05': { + 'private': 'dal05_private', 'public': 'dal05_public'}}) + def test_get_endpoint(self): + # Defaults to dal05, public + endpoint = self.manager.get_endpoint() + self.assertEqual(endpoint, 'https://dal05_public') + + endpoint = self.manager.get_endpoint(network='private') + self.assertEqual(endpoint, 'https://dal05_private') + + endpoint = self.manager.get_endpoint(datacenter='datacenter01') + self.assertEqual(endpoint, 'https://public_endpoint') + + endpoint = self.manager.get_endpoint(datacenter='datacenter01', + network='private') + self.assertEqual(endpoint, 'https://private_endpoint') + + endpoint = self.manager.get_endpoint(datacenter='datacenter01', + network='private') + self.assertEqual(endpoint, 'https://private_endpoint') + + # ERROR CASES + self.assertRaises( + TypeError, + self.manager.get_endpoint, datacenter='doesnotexist') + + self.assertRaises( + TypeError, + self.manager.get_endpoint, network='doesnotexist') + + # MessagingConnection + @patch('SoftLayer.managers.messaging.MessagingConnection') + def test_get_connection(self, conn): + queue_conn = self.manager.get_connection( + 'QUEUE_ACCOUNT_ID', 'USERNAME', 'API_KEY') + conn.assert_called_with( + 'QUEUE_ACCOUNT_ID', endpoint='https://dal05.mq.softlayer.net') + conn().authenticate.assert_called_with( + 'USERNAME', 'API_KEY') + self.assertEqual(queue_conn, conn()) + + @patch('SoftLayer.managers.messaging.requests.get') + def test_ping(self, get): + result = self.manager.ping() + + get.assert_called_with('https://dal05.mq.softlayer.net/v1/ping') + get().raise_for_status.assert_called_with() + self.assertTrue(result) + + +class MessagingConnectionTests(unittest.TestCase): + + def setUp(self): + self.conn = SoftLayer.managers.messaging.MessagingConnection( + 'acount_id', endpoint='endpoint') + self.auth = MagicMock() + self.conn.auth = self.auth + + def test_init(self): + self.assertEqual(self.conn.account_id, 'acount_id') + self.assertEqual(self.conn.endpoint, 'endpoint') + self.assertEqual(self.conn.auth, self.auth) + + @patch('SoftLayer.managers.messaging.requests.request') + def test_make_request(self, request): + resp = self.conn.make_request('GET', 'path') + request.assert_called_with( + 'GET', 'endpoint/v1/acount_id/path', + headers={ + 'Content-Type': 'application/json', + 'User-Agent': USER_AGENT}, + auth=self.auth) + request().raise_for_status.assert_called_with() + self.assertEqual(resp, request()) + + @patch('SoftLayer.managers.messaging.QueueAuth') + def test_authenticate(self, auth): + self.conn.authenticate('username', 'api_key', auth_token='auth_token') + + auth.assert_called_with( + 'endpoint/v1/acount_id/auth', 'username', 'api_key', + auth_token='auth_token') + auth().auth.assert_called_with() + self.assertEqual(self.conn.auth, auth()) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_stats(self, make_request): + content = { + '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() + + make_request.assert_called_with('get', 'stats/hour') + self.assertEqual(content, result) + + # Queue-based Tests + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_get_queues(self, make_request): + make_request().content = json.dumps(QUEUE_LIST) + result = self.conn.get_queues() + + make_request.assert_called_with('get', 'queues', params={}) + self.assertEqual(QUEUE_LIST, result) + + # with tags + result = self.conn.get_queues(tags=['tag1', 'tag2']) + + make_request.assert_called_with( + 'get', 'queues', params={'tags': 'tag1,tag2'}) + self.assertEqual(QUEUE_LIST, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_create_queue(self, make_request): + make_request().content = json.dumps(QUEUE_1) + result = self.conn.create_queue('example_queue') + + make_request.assert_called_with( + 'put', 'queues/example_queue', data='{}') + self.assertEqual(QUEUE_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_modify_queue(self, make_request): + make_request().content = json.dumps(QUEUE_1) + result = self.conn.modify_queue('example_queue') + + make_request.assert_called_with( + 'put', 'queues/example_queue', data='{}') + self.assertEqual(QUEUE_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_get_queue(self, make_request): + make_request().content = json.dumps(QUEUE_1) + result = self.conn.get_queue('example_queue') + + make_request.assert_called_with('get', 'queues/example_queue') + self.assertEqual(QUEUE_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_delete_queue(self, make_request): + result = self.conn.delete_queue('example_queue') + make_request.assert_called_with( + 'delete', 'queues/example_queue', params={}) + self.assertTrue(result) + + # With Force + result = self.conn.delete_queue('example_queue', force=True) + make_request.assert_called_with( + 'delete', 'queues/example_queue', params={'force': 1}) + self.assertTrue(result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_push_queue_message(self, make_request): + make_request().content = json.dumps(MESSAGE_1) + result = self.conn.push_queue_message('example_queue', '') + + make_request.assert_called_with( + 'post', 'queues/example_queue/messages', data='{"body": ""}') + self.assertEqual(MESSAGE_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_pop_message(self, make_request): + make_request().content = json.dumps(MESSAGE_1) + result = self.conn.pop_message('example_queue') + + make_request.assert_called_with( + 'get', 'queues/example_queue/messages', params={'batch': 1}) + self.assertEqual(MESSAGE_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_delete_message(self, make_request): + result = self.conn.delete_message('example_queue', MESSAGE_1['id']) + + make_request.assert_called_with( + 'delete', 'queues/example_queue/messages/%s' % MESSAGE_1['id']) + self.assertTrue(result) + + # Topic-based Tests + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_get_topics(self, make_request): + make_request().content = json.dumps(TOPIC_LIST) + result = self.conn.get_topics() + + make_request.assert_called_with('get', 'topics', params={}) + self.assertEqual(TOPIC_LIST, result) + + # with tags + result = self.conn.get_topics(tags=['tag1', 'tag2']) + + make_request.assert_called_with( + 'get', 'topics', params={'tags': 'tag1,tag2'}) + self.assertEqual(TOPIC_LIST, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_create_topic(self, make_request): + make_request().content = json.dumps(TOPIC_1) + result = self.conn.create_topic('example_topic') + + make_request.assert_called_with( + 'put', 'topics/example_topic', data='{}') + self.assertEqual(TOPIC_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_modify_topic(self, make_request): + make_request().content = json.dumps(TOPIC_1) + result = self.conn.modify_topic('example_topic') + + make_request.assert_called_with( + 'put', 'topics/example_topic', data='{}') + self.assertEqual(TOPIC_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_get_topic(self, make_request): + make_request().content = json.dumps(TOPIC_1) + result = self.conn.get_topic('example_topic') + + make_request.assert_called_with('get', 'topics/example_topic') + self.assertEqual(TOPIC_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_delete_topic(self, make_request): + result = self.conn.delete_topic('example_topic') + make_request.assert_called_with( + 'delete', 'topics/example_topic', params={}) + self.assertTrue(result) + + # With Force + result = self.conn.delete_topic('example_topic', force=True) + make_request.assert_called_with( + 'delete', 'topics/example_topic', params={'force': 1}) + self.assertTrue(result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_push_topic_message(self, make_request): + make_request().content = json.dumps(MESSAGE_1) + result = self.conn.push_topic_message('example_topic', '') + + make_request.assert_called_with( + 'post', 'topics/example_topic/messages', data='{"body": ""}') + self.assertEqual(MESSAGE_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_get_subscriptions(self, make_request): + make_request().content = json.dumps(SUBSCRIPTION_LIST) + result = self.conn.get_subscriptions('example_topic') + + make_request.assert_called_with( + 'get', 'topics/example_topic/subscriptions') + self.assertEqual(SUBSCRIPTION_LIST, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_create_subscription(self, make_request): + make_request().content = json.dumps(SUBSCRIPTION_1) + endpoint_details = { + 'account_id': 'test', + 'queue_name': 'topic_subscription_queue'} + result = self.conn.create_subscription( + 'example_topic', 'queue', **endpoint_details) + + make_request.assert_called_with( + 'post', 'topics/example_topic/subscriptions', + data=json.dumps({ + 'endpoint_type': 'queue', 'endpoint': endpoint_details})) + self.assertEqual(SUBSCRIPTION_1, result) + + @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + def test_delete_subscription(self, make_request): + make_request().content = json.dumps(SUBSCRIPTION_1) + result = self.conn.delete_subscription( + 'example_topic', SUBSCRIPTION_1['id']) + + make_request.assert_called_with( + 'delete', + 'topics/example_topic/subscriptions/%s' % SUBSCRIPTION_1['id']) + self.assertTrue(result) diff --git a/docs/api/managers/messaging.rst b/docs/api/managers/messaging.rst new file mode 100644 index 000000000..e04257917 --- /dev/null +++ b/docs/api/managers/messaging.rst @@ -0,0 +1,8 @@ +.. _messaging: + +SoftLayer.messaging +=================== +.. automodule:: SoftLayer.managers.messaging + :members: + :inherited-members: + :undoc-members: From 97143591556d7c63924fdfce523f9476ce33170c Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 2 Jul 2013 16:32:21 -0500 Subject: [PATCH 0102/3227] Fixes a couple review issues --- SoftLayer/CLI/modules/messaging.py | 6 ++-- SoftLayer/managers/messaging.py | 38 ++++++++++++------------- SoftLayer/tests/managers/queue_tests.py | 38 ++++++++++++------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/SoftLayer/CLI/modules/messaging.py b/SoftLayer/CLI/modules/messaging.py index cc1302d18..550c12738 100644 --- a/SoftLayer/CLI/modules/messaging.py +++ b/SoftLayer/CLI/modules/messaging.py @@ -21,8 +21,8 @@ COMMON_MESSAGING_ARGS = """Service Options: - --endpoint=NAME Endpoint name - --network=TYPE Network type, [Options: public, private] + --datacenter=NAME Datacenter, E.G.: dal05 + --network=TYPE Network type, [Options: public, private] """ @@ -98,7 +98,7 @@ class Ping(CLIRunnable): def execute(client, args): manager = MessagingManager(client) ok = manager.ping( - endpoint_name=args['--endpoint'], network=args['--network']) + endpoint_name=args['--datacenter'], network=args['--network']) if ok: return 'OK' else: diff --git a/SoftLayer/managers/messaging.py b/SoftLayer/managers/messaging.py index d5dbc7b53..ad5d7e9be 100644 --- a/SoftLayer/managers/messaging.py +++ b/SoftLayer/managers/messaging.py @@ -134,7 +134,7 @@ def __init__(self, id, endpoint=None): self.endpoint = endpoint self.auth = None - def make_request(self, method, path, **kwargs): + def _make_request(self, method, path, **kwargs): """ Make request. Generally not called directly :param method: HTTP Method @@ -145,8 +145,8 @@ def make_request(self, method, path, **kwargs): 'Content-Type': 'application/json', 'User-Agent': USER_AGENT, } - kwargs['headers'] = dict( - headers.items() + kwargs.get('headers', {}).items()) + headers.update(kwargs.get('headers', {})) + kwargs['headers'] = headers kwargs['auth'] = self.auth url = '/'.join((self.endpoint, 'v1', self.account_id, path)) @@ -172,7 +172,7 @@ def stats(self, period='hour'): :param period: 'hour', 'day', 'week', 'month' """ - r = self.make_request('get', 'stats/%s' % period) + r = self._make_request('get', 'stats/%s' % period) return json.loads(r.content) # QUEUE METHODS @@ -185,7 +185,7 @@ def get_queues(self, tags=None): params = {} if tags: params['tags'] = ','.join(tags) - r = self.make_request('get', 'queues', params=params) + r = self._make_request('get', 'queues', params=params) return json.loads(r.content) def create_queue(self, queue_name, **kwargs): @@ -197,7 +197,7 @@ def create_queue(self, queue_name, **kwargs): queue = {} queue.update(kwargs) data = json.dumps(queue) - r = self.make_request('put', 'queues/%s' % queue_name, data=data) + r = self._make_request('put', 'queues/%s' % queue_name, data=data) return json.loads(r.content) def modify_queue(self, queue_name, **kwargs): @@ -213,7 +213,7 @@ def get_queue(self, queue_name): :param queue_name: Queue Name """ - r = self.make_request('get', 'queues/%s' % queue_name) + r = self._make_request('get', 'queues/%s' % queue_name) return json.loads(r.content) def delete_queue(self, queue_name, force=False): @@ -226,7 +226,7 @@ def delete_queue(self, queue_name, force=False): params = {} if force: params['force'] = 1 - self.make_request('delete', 'queues/%s' % queue_name, params=params) + self._make_request('delete', 'queues/%s' % queue_name, params=params) return True def push_queue_message(self, queue_name, body, **kwargs): @@ -238,7 +238,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, + r = self._make_request('post', 'queues/%s/messages' % queue_name, data=json.dumps(message)) return json.loads(r.content) @@ -248,7 +248,7 @@ def pop_message(self, queue_name, count=1): :param queue_name: Queue Name :param count: (optional) number of messages to retrieve """ - r = self.make_request('get', 'queues/%s/messages' % queue_name, + r = self._make_request('get', 'queues/%s/messages' % queue_name, params={'batch': count}) return json.loads(r.content) @@ -258,7 +258,7 @@ def delete_message(self, queue_name, message_id): :param queue_name: Queue Name :param message_id: Message id """ - self.make_request('delete', 'queues/%s/messages/%s' + self._make_request('delete', 'queues/%s/messages/%s' % (queue_name, message_id)) return True @@ -272,7 +272,7 @@ def get_topics(self, tags=None): params = {} if tags: params['tags'] = ','.join(tags) - r = self.make_request('get', 'topics', params=params) + r = self._make_request('get', 'topics', params=params) return json.loads(r.content) def create_topic(self, topic_name, **kwargs): @@ -282,7 +282,7 @@ def create_topic(self, topic_name, **kwargs): :param dict \*\*kwargs: Topic options """ data = json.dumps(kwargs) - r = self.make_request('put', 'topics/%s' % topic_name, data=data) + r = self._make_request('put', 'topics/%s' % topic_name, data=data) return json.loads(r.content) def modify_topic(self, topic_name, **kwargs): @@ -298,7 +298,7 @@ def get_topic(self, topic_name): :param topic_name: Topic Name """ - r = self.make_request('get', 'topics/%s' % topic_name) + r = self._make_request('get', 'topics/%s' % topic_name) return json.loads(r.content) def delete_topic(self, topic_name, force=False): @@ -311,7 +311,7 @@ def delete_topic(self, topic_name, force=False): params = {} if force: params['force'] = 1 - self.make_request('delete', 'topics/%s' % topic_name, params=params) + self._make_request('delete', 'topics/%s' % topic_name, params=params) return True def push_topic_message(self, topic_name, body, **kwargs): @@ -323,7 +323,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, + r = self._make_request('post', 'topics/%s/messages' % topic_name, data=json.dumps(message)) return json.loads(r.content) @@ -332,7 +332,7 @@ def get_subscriptions(self, topic_name): :param topic_name: Topic Name """ - r = self.make_request('get', 'topics/%s/subscriptions' % topic_name) + r = self._make_request('get', 'topics/%s/subscriptions' % topic_name) return json.loads(r.content) def create_subscription(self, topic_name, type, **kwargs): @@ -342,7 +342,7 @@ def create_subscription(self, topic_name, type, **kwargs): :param type: type ('queue' or 'http') :param dict \*\*kwargs: Subscription options """ - r = self.make_request( + r = self._make_request( 'post', 'topics/%s/subscriptions' % topic_name, data=json.dumps({'endpoint_type': type, 'endpoint': kwargs})) return json.loads(r.content) @@ -353,7 +353,7 @@ def delete_subscription(self, topic_name, subscription_id): :param topic_name: Topic Name :param subscription_id: Subscription id """ - self.make_request('delete', 'topics/%s/subscriptions/%s' % + self._make_request('delete', 'topics/%s/subscriptions/%s' % (topic_name, subscription_id)) return True diff --git a/SoftLayer/tests/managers/queue_tests.py b/SoftLayer/tests/managers/queue_tests.py index d6a39b8de..d95fcf8fc 100644 --- a/SoftLayer/tests/managers/queue_tests.py +++ b/SoftLayer/tests/managers/queue_tests.py @@ -201,7 +201,7 @@ def test_init(self): @patch('SoftLayer.managers.messaging.requests.request') def test_make_request(self, request): - resp = self.conn.make_request('GET', 'path') + resp = self.conn._make_request('GET', 'path') request.assert_called_with( 'GET', 'endpoint/v1/acount_id/path', headers={ @@ -221,7 +221,7 @@ def test_authenticate(self, auth): auth().auth.assert_called_with() self.assertEqual(self.conn.auth, auth()) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_stats(self, make_request): content = { 'notifications': [{'key': [2012, 7, 27, 14, 31], 'value': 2}], @@ -233,7 +233,7 @@ def test_stats(self, make_request): self.assertEqual(content, result) # Queue-based Tests - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_get_queues(self, make_request): make_request().content = json.dumps(QUEUE_LIST) result = self.conn.get_queues() @@ -248,7 +248,7 @@ def test_get_queues(self, make_request): 'get', 'queues', params={'tags': 'tag1,tag2'}) self.assertEqual(QUEUE_LIST, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_create_queue(self, make_request): make_request().content = json.dumps(QUEUE_1) result = self.conn.create_queue('example_queue') @@ -257,7 +257,7 @@ def test_create_queue(self, make_request): 'put', 'queues/example_queue', data='{}') self.assertEqual(QUEUE_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_modify_queue(self, make_request): make_request().content = json.dumps(QUEUE_1) result = self.conn.modify_queue('example_queue') @@ -266,7 +266,7 @@ def test_modify_queue(self, make_request): 'put', 'queues/example_queue', data='{}') self.assertEqual(QUEUE_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_get_queue(self, make_request): make_request().content = json.dumps(QUEUE_1) result = self.conn.get_queue('example_queue') @@ -274,7 +274,7 @@ def test_get_queue(self, make_request): make_request.assert_called_with('get', 'queues/example_queue') self.assertEqual(QUEUE_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_delete_queue(self, make_request): result = self.conn.delete_queue('example_queue') make_request.assert_called_with( @@ -287,7 +287,7 @@ def test_delete_queue(self, make_request): 'delete', 'queues/example_queue', params={'force': 1}) self.assertTrue(result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_push_queue_message(self, make_request): make_request().content = json.dumps(MESSAGE_1) result = self.conn.push_queue_message('example_queue', '') @@ -296,7 +296,7 @@ def test_push_queue_message(self, make_request): 'post', 'queues/example_queue/messages', data='{"body": ""}') self.assertEqual(MESSAGE_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_pop_message(self, make_request): make_request().content = json.dumps(MESSAGE_1) result = self.conn.pop_message('example_queue') @@ -305,7 +305,7 @@ def test_pop_message(self, make_request): 'get', 'queues/example_queue/messages', params={'batch': 1}) self.assertEqual(MESSAGE_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_delete_message(self, make_request): result = self.conn.delete_message('example_queue', MESSAGE_1['id']) @@ -314,7 +314,7 @@ def test_delete_message(self, make_request): self.assertTrue(result) # Topic-based Tests - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_get_topics(self, make_request): make_request().content = json.dumps(TOPIC_LIST) result = self.conn.get_topics() @@ -329,7 +329,7 @@ def test_get_topics(self, make_request): 'get', 'topics', params={'tags': 'tag1,tag2'}) self.assertEqual(TOPIC_LIST, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_create_topic(self, make_request): make_request().content = json.dumps(TOPIC_1) result = self.conn.create_topic('example_topic') @@ -338,7 +338,7 @@ def test_create_topic(self, make_request): 'put', 'topics/example_topic', data='{}') self.assertEqual(TOPIC_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_modify_topic(self, make_request): make_request().content = json.dumps(TOPIC_1) result = self.conn.modify_topic('example_topic') @@ -347,7 +347,7 @@ def test_modify_topic(self, make_request): 'put', 'topics/example_topic', data='{}') self.assertEqual(TOPIC_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_get_topic(self, make_request): make_request().content = json.dumps(TOPIC_1) result = self.conn.get_topic('example_topic') @@ -355,7 +355,7 @@ def test_get_topic(self, make_request): make_request.assert_called_with('get', 'topics/example_topic') self.assertEqual(TOPIC_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_delete_topic(self, make_request): result = self.conn.delete_topic('example_topic') make_request.assert_called_with( @@ -368,7 +368,7 @@ def test_delete_topic(self, make_request): 'delete', 'topics/example_topic', params={'force': 1}) self.assertTrue(result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_push_topic_message(self, make_request): make_request().content = json.dumps(MESSAGE_1) result = self.conn.push_topic_message('example_topic', '') @@ -377,7 +377,7 @@ def test_push_topic_message(self, make_request): 'post', 'topics/example_topic/messages', data='{"body": ""}') self.assertEqual(MESSAGE_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_get_subscriptions(self, make_request): make_request().content = json.dumps(SUBSCRIPTION_LIST) result = self.conn.get_subscriptions('example_topic') @@ -386,7 +386,7 @@ def test_get_subscriptions(self, make_request): 'get', 'topics/example_topic/subscriptions') self.assertEqual(SUBSCRIPTION_LIST, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_create_subscription(self, make_request): make_request().content = json.dumps(SUBSCRIPTION_1) endpoint_details = { @@ -401,7 +401,7 @@ def test_create_subscription(self, make_request): 'endpoint_type': 'queue', 'endpoint': endpoint_details})) self.assertEqual(SUBSCRIPTION_1, result) - @patch('SoftLayer.managers.messaging.MessagingConnection.make_request') + @patch('SoftLayer.managers.messaging.MessagingConnection._make_request') def test_delete_subscription(self, make_request): make_request().content = json.dumps(SUBSCRIPTION_1) result = self.conn.delete_subscription( From 25ec71548691f1451a9d1ad2525bbb8e8dcf8995 Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 19 Jul 2013 14:37:24 -0500 Subject: [PATCH 0103/3227] Adding missing documentation --- SoftLayer/managers/hardware.py | 43 +++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 519e0ffc4..4717f0d24 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -255,6 +255,14 @@ def reload(self, id, post_uri=None): id=id) def change_port_speed(self, id, public, speed): + """ Allows you to change the port speed of a server's NICs. + + :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. + :param int speed: The port speed to set. + """ if public: func = self.hardware.setPublicNetworkInterfaceSpeed else: @@ -263,10 +271,16 @@ 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. + """ 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. + """ create_options = self._generate_create_dict(**kwargs) return self.client['Product_Order'].verifyOrder(create_options) @@ -288,7 +302,34 @@ def _generate_create_dict( self, server=None, hostname=None, domain=None, hourly=False, location=None, os=None, disks=None, port_speed=None, 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! + + :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 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 + hourly (default) or monthly. Only applies to bare + metal instances. + :param string location: The location string (data center) for the server + :param int os: The operating system to use + :param array disks: An array of disks for the server. Disks will be + added in the order specified. + :param int port_speed: The port speed for the server. + :param bool bare_metal: Flag to indicate if this is a bare metal server + or a dedicated server (default). + :param int ram: The amount of RAM to order. Only applies to dedicated + servers. + :param int package_id: The package_id to use for the server. This should + either be a chassis ID for dedicated servers or + the bare metal instance package ID, which can be + obtained by calling _get_bare_metal_package_id + :param int disk_controller: The disk controller to use. + """ arguments = ['server', 'hostname', 'domain', 'location', 'os', 'disks', 'port_speed', 'bare_metal', 'ram', 'package_id', 'disk_controller', 'server_core', 'disk0'] From 6a56d702e77e0971374e876956c11b484ccf336c Mon Sep 17 00:00:00 2001 From: Nathan Beittenmiller Date: Fri, 19 Jul 2013 14:40:50 -0500 Subject: [PATCH 0104/3227] PEP8 fixes --- SoftLayer/managers/hardware.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 4717f0d24..d51fdfb56 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -306,16 +306,17 @@ def _generate_create_dict( Translates a list of arguments into a dictionary necessary for creating a server. NOTE - 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 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 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 hourly (default) or monthly. Only applies to bare metal instances. - :param string location: The location string (data center) for the server + :param string location: The location string (data center) for the + server :param int os: The operating system to use :param array disks: An array of disks for the server. Disks will be added in the order specified. @@ -324,10 +325,11 @@ def _generate_create_dict( or a dedicated server (default). :param int ram: The amount of RAM to order. Only applies to dedicated servers. - :param int package_id: The package_id to use for the server. This should - either be a chassis ID for dedicated servers or - the bare metal instance package ID, which can be - obtained by calling _get_bare_metal_package_id + :param int package_id: The package_id to use for the server. This + should either be a chassis ID for dedicated + servers or the bare metal instance package ID, + which can be obtained by calling + _get_bare_metal_package_id :param int disk_controller: The disk controller to use. """ arguments = ['server', 'hostname', 'domain', 'location', 'os', 'disks', From 7012a9a26585c82895835377305b8dcd31628f97 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 19 Jul 2013 16:02:38 -0500 Subject: [PATCH 0105/3227] Moves verbose option. Warnings for deprecated methods API: Removes unused verbose flag (using python logging instead) API: Moves deprecated APIs to its own file: SoftLayer.deprecated API: Adds warnings for deprecated methods, functions, options --- SoftLayer/API.py | 164 ++------------------------------ SoftLayer/deprecated.py | 176 +++++++++++++++++++++++++++++++++++ SoftLayer/tests/api_tests.py | 6 -- SoftLayer/transport.py | 3 +- docs/api/client.rst | 4 + 5 files changed, 191 insertions(+), 162 deletions(-) create mode 100644 SoftLayer/deprecated.py diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 8962b8068..dbcae121b 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -10,8 +10,10 @@ USER_AGENT from SoftLayer.transport import make_xml_rpc_api_call from SoftLayer.exceptions import SoftLayerError +from SoftLayer.deprecated import DeprecatedClientMixin import os + __all__ = ['Client', 'BasicAuthentication', 'TokenAuthentication', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] @@ -69,7 +71,7 @@ def __repr__(self): return "" % (self.username) -class Client(object): +class Client(DeprecatedClientMixin, object): """ A SoftLayer API client. :param service_name: the name of the SoftLayer API service to query @@ -84,7 +86,6 @@ class Client(object): Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. :param integer timeout: timeout for API requests - :param boolean verbose: prints details about every HTTP request if true :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. Example: `BasicAuthentication` @@ -100,9 +101,8 @@ class Client(object): _prefix = "SoftLayer_" def __init__(self, service_name=None, id=None, username=None, api_key=None, - endpoint_url=None, timeout=None, verbose=False, auth=None): + endpoint_url=None, timeout=None, auth=None): self._service_name = service_name - self.verbose = verbose self._headers = {} self._raw_headers = {} @@ -113,15 +113,16 @@ def __init__(self, service_name=None, id=None, username=None, api_key=None, api_key = api_key or API_KEY or os.environ.get('SL_API_KEY') or '' if username and api_key: self.auth = BasicAuthentication(username, api_key) - self.set_authentication(username, api_key) - - if id is not None: - self.set_init_parameter(int(id)) self._endpoint_url = (endpoint_url or API_BASE_URL or 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): @@ -143,125 +144,6 @@ def authenticate_with_password(self, username, password, self.auth = TokenAuthentication(res['userId'], res['hash']) return (res['userId'], res['hash']) - 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 - - """ - 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 - - """ - 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 - - """ - 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 - - """ - 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 - - """ - 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 - - """ - 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 - - """ - self.add_header('resultLimit', { - 'limit': int(limit), - 'offset': int(offset) - }) - def __getitem__(self, name): """ Get a SoftLayer Service. @@ -343,8 +225,7 @@ def call(self, service, method, *args, **kwargs): return make_xml_rpc_api_call(uri, method, args, headers=headers, http_headers=http_headers, - timeout=self.timeout, - verbose=self.verbose) + timeout=self.timeout) __call__ = call @@ -424,31 +305,6 @@ def __format_object_mask(self, objectmask, service): return {mheader: {'mask': objectmask}} - 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 - - """ - 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 __repr__(self): return "" \ % (self._endpoint_url, self.auth) diff --git a/SoftLayer/deprecated.py b/SoftLayer/deprecated.py new file mode 100644 index 000000000..0f28e6510 --- /dev/null +++ b/SoftLayer/deprecated.py @@ -0,0 +1,176 @@ +""" + 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.set_authentication(username, api_key) + + 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 + + """ + 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 e6bf9ff2e..d63248491 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -127,7 +127,6 @@ def test_old_api(self, make_xml_rpc_api_call): headers={ 'authenticate': { 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}}, - verbose=False, timeout=None, http_headers={ 'Content-Type': 'application/xml', @@ -164,7 +163,6 @@ def test_complex_old_api(self, make_xml_rpc_api_call): 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, 'SoftLayer_SERVICEInitParameters': {'id': 5678}, 'resultLimit': {'limit': 9, 'offset': 10}}, - verbose=False, timeout=None, http_headers={ 'RAW': 'HEADER', @@ -192,7 +190,6 @@ def test_simple_call(self, make_xml_rpc_api_call): headers={ 'authenticate': { 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}}, - verbose=False, timeout=None, http_headers={ 'Content-Type': 'application/xml', @@ -222,7 +219,6 @@ def test_complex(self, make_xml_rpc_api_call): 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, 'SoftLayer_SERVICEInitParameters': {'id': 5678}, 'resultLimit': {'limit': 9, 'offset': 10}}, - verbose=False, timeout=None, http_headers={ 'RAW': 'HEADER', @@ -240,7 +236,6 @@ def test_mask_call_v2(self, make_xml_rpc_api_call): 'authenticate': { 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, 'SoftLayer_ObjectMask': {'mask': 'mask[something[nested]]'}}, - verbose=False, timeout=None, http_headers={ 'Content-Type': 'application/xml', @@ -257,7 +252,6 @@ def test_mask_call_v2_dot(self, make_xml_rpc_api_call): 'authenticate': { 'username': 'doesnotexist', 'apiKey': 'issurelywrong'}, 'SoftLayer_ObjectMask': {'mask': 'mask[something.nested]'}}, - verbose=False, timeout=None, http_headers={ 'Content-Type': 'application/xml', diff --git a/SoftLayer/transport.py b/SoftLayer/transport.py index 35317e149..3b3984f27 100644 --- a/SoftLayer/transport.py +++ b/SoftLayer/transport.py @@ -19,7 +19,7 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, - http_headers=None, timeout=None, verbose=False): + http_headers=None, timeout=None): """ Makes a SoftLayer API call against the XML-RPC endpoint :param string uri: endpoint URL @@ -27,7 +27,6 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, :param dict headers: XML-RPC headers to use for the request :param dict http_headers: HTTP headers to use for the request :param int timeout: number of seconds to use as a timeout - :param bool verbose: verbosity """ if args is None: args = tuple() diff --git a/docs/api/client.rst b/docs/api/client.rst index af405c41b..dfdf115cd 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -125,6 +125,10 @@ 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: + :: import SoftLayer.API From fb08160416d0b32043a5dbf0be555b05425392e0 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 19 Jul 2013 16:49:29 -0500 Subject: [PATCH 0106/3227] API: Adds deprecation warning to set_authentication() --- SoftLayer/deprecated.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SoftLayer/deprecated.py b/SoftLayer/deprecated.py index 0f28e6510..1c236e230 100644 --- a/SoftLayer/deprecated.py +++ b/SoftLayer/deprecated.py @@ -22,7 +22,10 @@ def __init__(self, id=None, username=None, api_key=None, **kwargs): self.set_init_parameter(int(id)) if username and api_key: - self.set_authentication(username, api_key) + self._headers['authenticate'] = { + 'username': username.strip(), + 'apiKey': api_key.strip(), + } def __getattr__(self, name): """ Attempt a SoftLayer API call. @@ -105,6 +108,7 @@ def set_authentication(self, username, api_key): .. deprecated:: 2.0.0 """ + warn("deprecated", DeprecationWarning) self.add_header('authenticate', { 'username': username.strip(), 'apiKey': api_key.strip(), From 17abaa0fcda2de9fe18a99a1746474da402af76e Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 19 Jul 2013 16:55:32 -0500 Subject: [PATCH 0107/3227] 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 0108/3227] 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 0109/3227] 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 0110/3227] 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 0111/3227] 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 0112/3227] 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 0113/3227] 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 0114/3227] 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 0115/3227] 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 0116/3227] 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 0117/3227] 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 0118/3227] 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 0119/3227] 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 0120/3227] 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 0121/3227] 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 0122/3227] 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 0123/3227] 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 0124/3227] 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 0125/3227] 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 0126/3227] 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 0127/3227] 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 0128/3227] 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 0129/3227] 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 0130/3227] 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 0131/3227] 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 0132/3227] 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 0133/3227] 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 0134/3227] 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 0135/3227] 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 0136/3227] 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 0137/3227] 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 0138/3227] 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 0139/3227] 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 0140/3227] 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 0141/3227] 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 0142/3227] 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 0143/3227] 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 0144/3227] 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 0145/3227] 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 0146/3227] 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 0147/3227] 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 0148/3227] 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 0149/3227] 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 0150/3227] 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 0151/3227] 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 0152/3227] 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 0153/3227] 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 0154/3227] 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 0155/3227] 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 0156/3227] 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 0157/3227] 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 0158/3227] 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 0159/3227] 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 0160/3227] 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 0161/3227] 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 0162/3227] 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 0163/3227] 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 0164/3227] 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 0165/3227] 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 0166/3227] 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 0167/3227] 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 0168/3227] 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