From e7de42b7429a58312e693f56aa78b4c950839a67 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 16 Dec 2024 18:52:03 -0600 Subject: [PATCH 1/2] Added some exception handling for XML data that has too large of an INT in it. #2201 --- .../fixtures/SoftLayer_Billing_Invoice.py | 58 ++++++++++++++----- SoftLayer/testing/xmlrpc.py | 26 ++++++++- SoftLayer/transports/xmlrpc.py | 3 +- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/SoftLayer/fixtures/SoftLayer_Billing_Invoice.py b/SoftLayer/fixtures/SoftLayer_Billing_Invoice.py index d4d89131c..eb9e1171d 100644 --- a/SoftLayer/fixtures/SoftLayer_Billing_Invoice.py +++ b/SoftLayer/fixtures/SoftLayer_Billing_Invoice.py @@ -1,23 +1,49 @@ getInvoiceTopLevelItems = [ { - 'categoryCode': 'sov_sec_ip_addresses_priv', - 'createDate': '2018-04-04T23:15:20-06:00', - 'description': '64 Portable Private IP Addresses', - 'id': 724951323, - 'oneTimeAfterTaxAmount': '0', - 'recurringAfterTaxAmount': '0', - 'hostName': 'bleg', - 'domainName': 'beh.com', - 'category': {'name': 'Private (only) Secondary VLAN IP Addresses'}, - 'children': [ + "categoryCode": "sov_sec_ip_addresses_priv", + "createDate": "2018-04-04T23:15:20-06:00", + "description": "64 Portable Private IP Addresses", + "id": 724951323, + "oneTimeAfterTaxAmount": "0", + "recurringAfterTaxAmount": "0", + "hostName": "bleg", + "domainName": "beh.com", + "category": {"name": "Private (only) Secondary VLAN IP Addresses"}, + "children": [ { - 'id': 12345, - 'category': {'name': 'Fake Child Category'}, - 'description': 'Blah', - 'oneTimeAfterTaxAmount': 55.50, - 'recurringAfterTaxAmount': 0.10 + "id": 12345, + "category": {"name": "Fake Child Category"}, + "description": "Blah", + "oneTimeAfterTaxAmount": 55.50, + "recurringAfterTaxAmount": 0.10 } ], - 'location': {'name': 'fra02'} + "location": {"name": "fra02"} + }, + { + "categoryCode": "reserved_capacity", + "createDate": "2024-07-03T22:08:36-07:00", + "description": "B1.1x2 (1 Year Term) (721hrs * .025)", + "id": 1111222, + "oneTimeAfterTaxAmount": "0", + "recurringAfterTaxAmount": "18.03", + "category": {"name": "Reserved Capacity"}, + "children": [ + { + "description": "1 x 2.0 GHz or higher Core", + "id": 29819, + "oneTimeAfterTaxAmount": "0", + "recurringAfterTaxAmount": "10.00", + "category": {"name": "Computing Instance"} + }, + { + "description": "2 GB", + "id": 123456, + "oneTimeAfterTaxAmount": "0", + "recurringAfterTaxAmount": "2.33", + "category": {"name": "RAM"} + } + ], + "location": {"name": "dal10"} } ] diff --git a/SoftLayer/testing/xmlrpc.py b/SoftLayer/testing/xmlrpc.py index a572fd79d..fb8f1ccc0 100644 --- a/SoftLayer/testing/xmlrpc.py +++ b/SoftLayer/testing/xmlrpc.py @@ -3,6 +3,20 @@ ~~~~~~~~~~~~~~~~~~~~~~~~ XMP-RPC server which can use a transport to proxy requests for testing. + If you want to spin up a test XML server to make fake API calls with, try this: + + quick-server.py + --- + import SoftLayer + from SoftLayer.testing import xmlrpc + + my_xport = SoftLayer.FixtureTransport() + my_server = xmlrpc.create_test_server(my_xport, "localhost", port=4321) + print(f"Server running on http://{my_server.server_name}:{my_server.server_port}") + --- + $> python quick-server.py + $> curl -X POST -d "getInvoiceTopLevelItemsheadersSoftLayer_Billing_InvoiceInitParametersid1234" http://127.0.0.1:4321/SoftLayer_Billing_Invoice + :license: MIT, see LICENSE for more details. """ import http.server @@ -60,6 +74,7 @@ def do_POST(self): self.send_response(200) self.send_header("Content-type", "application/xml; charset=UTF-8") self.end_headers() + try: self.wfile.write(response_body.encode('utf-8')) except UnicodeDecodeError: @@ -78,9 +93,16 @@ def do_POST(self): response = xmlrpc.client.Fault(ex.faultCode, str(ex.reason)) response_body = xmlrpc.client.dumps(response, allow_none=True, methodresponse=True) self.wfile.write(response_body.encode('utf-8')) - except Exception: + except OverflowError as ex: + self.send_response(555) + self.send_header("Content-type", "application/xml; charset=UTF-8") + self.end_headers() + response_body = '''OverflowError in XML response.''' + self.wfile.write(response_body.encode('utf-8')) + logging.exception(f"Error while handling request: {str(ex)}") + except Exception as ex: self.send_response(500) - logging.exception("Error while handling request") + logging.exception(f"Error while handling request: {str(ex)}") def log_message(self, fmt, *args): """Override log_message.""" diff --git a/SoftLayer/transports/xmlrpc.py b/SoftLayer/transports/xmlrpc.py index 66cdb5707..16456eda9 100644 --- a/SoftLayer/transports/xmlrpc.py +++ b/SoftLayer/transports/xmlrpc.py @@ -121,7 +121,8 @@ def __call__(self, request): _ex = error_mapping.get(ex.faultCode, exceptions.SoftLayerAPIError) raise _ex(ex.faultCode, ex.faultString) from ex except requests.HTTPError as ex: - raise exceptions.TransportError(ex.response.status_code, str(ex)) + err_message = f"{str(ex)} :: {ex.response.content}" + raise exceptions.TransportError(ex.response.status_code, err_message) except requests.RequestException as ex: raise exceptions.TransportError(0, str(ex)) From 413fa885b5f263bc68b2939623399cd092892669 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 17 Dec 2024 14:25:49 -0600 Subject: [PATCH 2/2] Fixed #2201 Added invoice item rollup to 'account invoice-detail' to better match invoices as dispalyed in the portal --- SoftLayer/CLI/account/invoice_detail.py | 40 +++++++++++++++++++++++-- SoftLayer/testing/xmlrpc.py | 11 +++++-- tests/CLI/modules/account_tests.py | 17 ++++++++--- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/SoftLayer/CLI/account/invoice_detail.py b/SoftLayer/CLI/account/invoice_detail.py index 281940ee5..4436c44d9 100644 --- a/SoftLayer/CLI/account/invoice_detail.py +++ b/SoftLayer/CLI/account/invoice_detail.py @@ -16,7 +16,13 @@ help="Shows a very detailed list of charges") @environment.pass_env def cli(env, identifier, details): - """Invoice details""" + """Invoice details + + Will display the top level invoice items for a given invoice. The cost displayed is the sum of the item's + cost along with all its child items. + The --details option will display any child items a top level item may have. Parent items will appear + in this list as well to display their specific cost. + """ manager = AccountManager(env.client) top_items = manager.get_billing_items(identifier) @@ -49,16 +55,31 @@ def get_invoice_table(identifier, top_items, details): description = nice_string(item.get('description')) if fqdn != '.': description = "%s (%s)" % (item.get('description'), fqdn) + total_recur, total_single = sum_item_charges(item) table.add_row([ item.get('id'), category, nice_string(description), - "$%.2f" % float(item.get('oneTimeAfterTaxAmount')), - "$%.2f" % float(item.get('recurringAfterTaxAmount')), + f"${total_single:,.2f}", + f"${total_recur:,.2f}", utils.clean_time(item.get('createDate'), out_format="%Y-%m-%d"), utils.lookup(item, 'location', 'name') ]) if details: + # This item has children, so we want to print out the parent item too. This will match the + # invoice from the portal. https://github.com/softlayer/softlayer-python/issues/2201 + if len(item.get('children')) > 0: + single = float(item.get('oneTimeAfterTaxAmount', 0.0)) + recurring = float(item.get('recurringAfterTaxAmount', 0.0)) + table.add_row([ + '>>>', + category, + nice_string(description), + f"${single:,.2f}", + f"${recurring:,.2f}", + '---', + '---' + ]) for child in item.get('children', []): table.add_row([ '>>>', @@ -70,3 +91,16 @@ def get_invoice_table(identifier, top_items, details): '---' ]) return table + + +def sum_item_charges(item: dict) -> (float, float): + """Takes a billing Item, sums up its child items and returns recurring, one_time prices""" + + # API returns floats as strings in this case + single = float(item.get('oneTimeAfterTaxAmount', 0.0)) + recurring = float(item.get('recurringAfterTaxAmount', 0.0)) + for child in item.get('children', []): + single = single + float(child.get('oneTimeAfterTaxAmount', 0.0)) + recurring = recurring + float(child.get('recurringAfterTaxAmount', 0.0)) + + return (recurring, single) diff --git a/SoftLayer/testing/xmlrpc.py b/SoftLayer/testing/xmlrpc.py index fb8f1ccc0..e0e7e5ca2 100644 --- a/SoftLayer/testing/xmlrpc.py +++ b/SoftLayer/testing/xmlrpc.py @@ -15,7 +15,12 @@ print(f"Server running on http://{my_server.server_name}:{my_server.server_port}") --- $> python quick-server.py - $> curl -X POST -d "getInvoiceTopLevelItemsheadersSoftLayer_Billing_InvoiceInitParametersid1234" http://127.0.0.1:4321/SoftLayer_Billing_Invoice + $> curl -X POST -d " \ +getInvoiceTopLevelItemsheaders \ +SoftLayer_Billing_InvoiceInitParameters \ +id1234 \ +" \ +http://127.0.0.1:4321/SoftLayer_Billing_Invoice :license: MIT, see LICENSE for more details. """ @@ -99,10 +104,10 @@ def do_POST(self): self.end_headers() response_body = '''OverflowError in XML response.''' self.wfile.write(response_body.encode('utf-8')) - logging.exception(f"Error while handling request: {str(ex)}") + logging.exception("Error while handling request: %s", ex) except Exception as ex: self.send_response(500) - logging.exception(f"Error while handling request: {str(ex)}") + logging.exception("Error while handling request: %s", ex) def log_message(self, fmt, *args): """Override log_message.""" diff --git a/tests/CLI/modules/account_tests.py b/tests/CLI/modules/account_tests.py index 06c718cb4..9dd4dd905 100644 --- a/tests/CLI/modules/account_tests.py +++ b/tests/CLI/modules/account_tests.py @@ -44,14 +44,11 @@ def test_event_jsonraw_output(self): command = '--format jsonraw account events' command_params = command.split() result = self.run_command(command_params) - json_text_tables = result.stdout.split('\n') - print(f"RESULT: {result.output}") # removing an extra item due to an additional Newline at the end of the output json_text_tables.pop() # each item in the json_text_tables should be a list for json_text_table in json_text_tables: - print(f"TESTING THIS: \n{json_text_table}\n") json_table = json.loads(json_text_table) self.assertIsInstance(json_table, list) @@ -66,6 +63,18 @@ def test_invoice_detail_details(self): self.assert_no_fail(result) self.assert_called_with('SoftLayer_Billing_Invoice', 'getInvoiceTopLevelItems', identifier='1234') + def test_invoice_detail_sum_children(self): + result = self.run_command(['--format=json', 'account', 'invoice-detail', '1234', '--details']) + self.assert_no_fail(result) + json_out = json.loads(result.output) + self.assertEqual(len(json_out), 7) + self.assertEqual(json_out[0]['Item Id'], 724951323) + self.assertEqual(json_out[0]['Single'], '$55.50') + self.assertEqual(json_out[0]['Monthly'], '$0.10') + self.assertEqual(json_out[3]['Item Id'], 1111222) + self.assertEqual(json_out[3]['Single'], '$0.00') + self.assertEqual(json_out[3]['Monthly'], '$30.36') + def test_invoice_detail_csv_output_format(self): result = self.run_command(["--format", "csv", 'account', 'invoice-detail', '1234']) result_output = result.output.replace('\r', '').split('\n') @@ -74,7 +83,7 @@ def test_invoice_detail_csv_output_format(self): '"Create Date","Location"') self.assertEqual(result_output[1], '724951323,"Private (only) Secondary VLAN IP Addresses",' '"64 Portable Private IP Addresses (bleg.beh.com)",' - '"$0.00","$0.00","2018-04-04","fra02"') + '"$55.50","$0.10","2018-04-04","fra02"') # slcli account invoices def test_invoices(self):