From 82116c1d04c92655b70e5bfc4545d1952baefb38 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 15 May 2024 17:59:01 -0500 Subject: [PATCH 01/77] Fixed a bug in the config parser when the Verify option was not specified. Fixed #2149 --- SoftLayer/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/config.py b/SoftLayer/config.py index 97ccb9d0c..e695d6b9a 100644 --- a/SoftLayer/config.py +++ b/SoftLayer/config.py @@ -61,7 +61,7 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'proxy': '', 'userid': '', 'access_token': '', - 'verify': True + 'verify': "True" }) config.read(config_files) From ec9291b1e379a6a542fc38ddfbccd7864cc6fb83 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 15 May 2024 19:16:31 -0500 Subject: [PATCH 02/77] v6.2.2 release. Also changed HTTPAdapter to only retry 3 times --- SoftLayer/consts.py | 2 +- SoftLayer/transports/transport.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 79cd61ef7..2bc9bd20a 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v6.2.1' +VERSION = 'v6.2.2' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index 0454632ba..ab5ebedde 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -23,7 +23,7 @@ def get_session(user_agent): 'Content-Type': 'application/json', 'User-Agent': user_agent, }) - retry = Retry(connect=3, backoff_factor=3) + retry = Retry(total=3, connect=1, backoff_factor=1) adapter = HTTPAdapter(max_retries=retry) client.mount('https://', adapter) return client diff --git a/setup.py b/setup.py index 5e57aaa48..d4490fe20 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='v6.2.1', + version='v6.2.2', description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type='text/x-rst', From c02bb72b574177f456e9a8b403a46530d85ba178 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 16 May 2024 09:36:38 -0500 Subject: [PATCH 03/77] Fixed tox issues --- SoftLayer/CLI/block/order.py | 1 + SoftLayer/CLI/file/order.py | 33 ++++++++++------------------ SoftLayer/CLI/firewall/add.py | 2 +- SoftLayer/CLI/firewall/detail.py | 1 - SoftLayer/managers/dedicated_host.py | 4 +++- SoftLayer/managers/hardware.py | 1 + 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py index 528e4e1f8..bdd0100e0 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -78,6 +78,7 @@ def cli(env, storage_type, size, iops, tier, os_type, 'Hourly billing is only available for the storage_as_a_service service offering' ) + order = {} if storage_type == 'performance': if iops is None: raise exceptions.CLIAbort('Option --iops required with Performance') diff --git a/SoftLayer/CLI/file/order.py b/SoftLayer/CLI/file/order.py index cca7baf5e..966eb6542 100644 --- a/SoftLayer/CLI/file/order.py +++ b/SoftLayer/CLI/file/order.py @@ -12,26 +12,18 @@ @click.command(cls=SoftLayer.CLI.command.SLCommand, context_settings=CONTEXT_SETTINGS) -@click.option('--storage-type', - help='Type of file storage volume', - type=click.Choice(['performance', 'endurance']), - required=True) -@click.option('--size', - type=int, - help='Size of file storage volume in GB', - required=True) -@click.option('--iops', - type=int, +@click.option('--storage-type', required=True, type=click.Choice(['performance', 'endurance']), + help='Type of file storage volume') +@click.option('--size', type=int, required=True, + help='Size of file storage volume in GB') +@click.option('--iops', type=int, help="""Performance Storage IOPs. Options vary based on storage size. [required for storage-type performance]""") -@click.option('--tier', - help='Endurance Storage Tier (IOP per GB) [required for storage-type endurance]', - type=click.Choice(['0.25', '2', '4', '10'])) -@click.option('-l', '--location', - help='Datacenter short name (e.g.: dal09)', - required=True) -@click.option('--snapshot-size', - type=int, +@click.option('--tier', type=click.Choice(['0.25', '2', '4', '10']), + help='Endurance Storage Tier (IOP per GB) [required for storage-type endurance]') +@click.option('-l', '--location', required=True, + help='Datacenter short name (e.g.: dal09)') +@click.option('--snapshot-size', type=int, help='Optional parameter for ordering snapshot ' 'space along with endurance file storage; specifies ' 'the size (in GB) of snapshot space to order') @@ -43,9 +35,7 @@ 'storage_as_a_service', 'enterprise', 'performance'])) -@click.option('--billing', - type=click.Choice(['hourly', 'monthly']), - default='monthly', +@click.option('--billing', type=click.Choice(['hourly', 'monthly']), default='monthly', help="Optional parameter for Billing rate (default to monthly)") @click.option('--force', default=False, is_flag=True, help="Force order file storage volume without confirmation") @environment.pass_env @@ -83,6 +73,7 @@ def cli(env, storage_type, size, iops, tier, 'Hourly billing is only available for the storage_as_a_service service offering' ) + order = {} if storage_type == 'performance': if iops is None: raise exceptions.CLIAbort('Option --iops required with Performance') diff --git a/SoftLayer/CLI/firewall/add.py b/SoftLayer/CLI/firewall/add.py index 9a9966d82..3df2da32c 100644 --- a/SoftLayer/CLI/firewall/add.py +++ b/SoftLayer/CLI/firewall/add.py @@ -22,7 +22,7 @@ def cli(env, target, firewall_type, high_availability, force): """ mgr = SoftLayer.FirewallManager(env.client) - + pkg = {} if not env.skip_confirmations: if firewall_type == 'vlan': pkg = mgr.get_dedicated_package(ha_enabled=high_availability) diff --git a/SoftLayer/CLI/firewall/detail.py b/SoftLayer/CLI/firewall/detail.py index c07695d2b..8f0d276dd 100644 --- a/SoftLayer/CLI/firewall/detail.py +++ b/SoftLayer/CLI/firewall/detail.py @@ -82,7 +82,6 @@ def cli(env, identifier, password): else: click.secho('Invalid firewall type %s: firewall type should be either vlan, multiVlan, vs or server.' % firewall_type, fg='red') - return def get_rules_table(rules): diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index f9eda0325..6decff128 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -431,7 +431,9 @@ def _get_backend_router(self, locations, item): hostname ''' cpu_count = item['capacity'] - + mem_capacity = {} + disk_capacity = {} + gpuComponents = {} for capacity in item['bundleItems']: for category in capacity['categories']: if category['categoryCode'] == 'dedicated_host_ram': diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 69add1cf7..6564fda0f 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -1123,6 +1123,7 @@ def _get_disk_price_detail(self, disk_data, upgrade_prices, disk_channel, disk_t :param String disk_type: Disk type. """ + disk_price = {} if disk_data.get('description') == disk_type: if "add" in disk_type: raise SoftLayerError("Unable to add the disk because this already exists.") From 94784c6e634e1435ce85171a4650750e243a22cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 21:18:35 +0000 Subject: [PATCH 04/77] --- updated-dependencies: - dependency-name: sphinx-click dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 0f8148172..a421e1702 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ sphinx==7.3.7 sphinx_rtd_theme==2.0.0 -sphinx-click==5.1.0 +sphinx-click==6.0.0 click prettytable rich From 6d6ab5ac464ce42e1f0d62d619d67cce549d458b Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 28 May 2024 17:17:34 -0500 Subject: [PATCH 05/77] Fixed #2154 added more error handling around table output --- SoftLayer/CLI/block/detail.py | 24 ++-- SoftLayer/CLI/file/detail.py | 78 ++++--------- SoftLayer/CLI/formatting.py | 11 +- .../fixtures/SoftLayer_Network_Storage.py | 103 ++++++++++++++++++ tests/CLI/formatting_table_tests.py | 44 ++++++++ tests/CLI/modules/file_tests.py | 8 ++ 6 files changed, 197 insertions(+), 71 deletions(-) diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py index 7752e1b56..263e21b48 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -83,25 +83,21 @@ def cli(env, volume_id): table.add_row(['Replication Status', "%s" % block_volume['replicationStatus']]) + replicant_table = formatting.KeyValueTable(['Name', 'Value']) + replicant_table.align['Name'] = 'r' + replicant_table.align['Value'] = 'l' for replicant in block_volume['replicationPartners']: - replicant_table = formatting.Table(['Name', - 'Value']) - replicant_table.add_row(['Replicant Id', replicant['id']]) replicant_table.add_row([ - 'Volume Name', - utils.lookup(replicant, 'username')]) + 'Replicant Id', replicant['id']]) replicant_table.add_row([ - 'Target IP', - utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) + 'Volume Name', utils.lookup(replicant, 'username')]) replicant_table.add_row([ - 'Data Center', - utils.lookup(replicant, - 'serviceResource', 'datacenter', 'name')]) + 'Target IP', utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) replicant_table.add_row([ - 'Schedule', - utils.lookup(replicant, - 'replicationSchedule', 'type', 'keyname')]) - table.add_row(['Replicant Volumes', replicant_table]) + 'Data Center', utils.lookup(replicant, 'serviceResource', 'datacenter', 'name')]) + replicant_table.add_row([ + 'Schedule', utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname')]) + table.add_row(['Replicant Volumes', replicant_table]) if block_volume.get('originalVolumeSize'): original_volume_info = formatting.Table(['Property', 'Value']) diff --git a/SoftLayer/CLI/file/detail.py b/SoftLayer/CLI/file/detail.py index cc4860452..009dc1fc9 100644 --- a/SoftLayer/CLI/file/detail.py +++ b/SoftLayer/CLI/file/detail.py @@ -37,54 +37,27 @@ def cli(env, volume_id): table.add_row(['Type', storage_type]) table.add_row(['Capacity (GB)', get_capacity(file_volume)]) - used_space = int(file_volume['bytesUsed']) \ - if file_volume['bytesUsed'] else 0 - if used_space < (1 << 10): - table.add_row(['Used Space', "%dB" % used_space]) - elif used_space < (1 << 20): - table.add_row(['Used Space', "%dKB" % (used_space / (1 << 10))]) - elif used_space < (1 << 30): - table.add_row(['Used Space', "%dMB" % (used_space / (1 << 20))]) - else: - table.add_row(['Used Space', "%dGB" % (used_space / (1 << 30))]) + used_space = formatting.convert_sizes(file_volume.get('bytes_used', 0), "GB", False) + table.add_row(['Used Space', used_space]) if file_volume.get('provisionedIops'): table.add_row(['IOPs', float(file_volume['provisionedIops'])]) if file_volume.get('storageTierLevel'): - table.add_row([ - 'Endurance Tier', - file_volume['storageTierLevel'], - ]) - - table.add_row([ - 'Data Center', - file_volume['serviceResource']['datacenter']['name'], - ]) - table.add_row([ - 'Target IP', - file_volume['serviceResourceBackendIpAddress'], - ]) + table.add_row(['Endurance Tier', file_volume['storageTierLevel']]) + + table.add_row(['Data Center', file_volume['serviceResource']['datacenter']['name']]) + table.add_row(['Target IP', file_volume['serviceResourceBackendIpAddress']]) if file_volume['fileNetworkMountAddress']: - table.add_row([ - 'Mount Address', - file_volume['fileNetworkMountAddress'], - ]) + table.add_row(['Mount Address', file_volume['fileNetworkMountAddress']]) if file_volume['snapshotCapacityGb']: - table.add_row([ - 'Snapshot Capacity (GB)', - file_volume['snapshotCapacityGb'], - ]) + table.add_row(['Snapshot Capacity (GB)', file_volume['snapshotCapacityGb']]) if 'snapshotSizeBytes' in file_volume['parentVolume']: - table.add_row([ - 'Snapshot Used (Bytes)', - file_volume['parentVolume']['snapshotSizeBytes'], - ]) + table.add_row(['Snapshot Used (Bytes)', file_volume['parentVolume']['snapshotSizeBytes']]) - table.add_row(['# of Active Transactions', "%i" - % file_volume['activeTransactionCount']]) + table.add_row(["# of Active Transactions", file_volume['activeTransactionCount']]) if file_volume['activeTransactions']: for trans in file_volume['activeTransactions']: @@ -98,32 +71,25 @@ def cli(env, volume_id): # returns a string or object for 'replicationStatus'; it seems that # the type is string for File volumes and object for Block volumes if 'message' in file_volume['replicationStatus']: - table.add_row(['Replication Status', "%s" - % file_volume['replicationStatus']['message']]) + table.add_row(['Replication Status', file_volume['replicationStatus']['message']]) else: - table.add_row(['Replication Status', "%s" - % file_volume['replicationStatus']]) + table.add_row(['Replication Status', file_volume['replicationStatus']]) - replicant_list = [] + replicant_table = formatting.KeyValueTable(['Name', 'Value']) + replicant_table.align['Name'] = 'r' + replicant_table.align['Value'] = 'l' for replicant in file_volume['replicationPartners']: - replicant_table = formatting.Table(['Replicant ID', - replicant['id']]) replicant_table.add_row([ - 'Volume Name', - utils.lookup(replicant, 'username')]) + 'Volume ID', replicant.get('id')]) + replicant_table.add_row([ + 'Volume Name', utils.lookup(replicant, 'username')]) replicant_table.add_row([ - 'Target IP', - utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) + 'Target IP', utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) replicant_table.add_row([ - 'Data Center', - utils.lookup(replicant, - 'serviceResource', 'datacenter', 'name')]) + 'Data Center', utils.lookup(replicant, 'serviceResource', 'datacenter', 'name')]) replicant_table.add_row([ - 'Schedule', - utils.lookup(replicant, - 'replicationSchedule', 'type', 'keyname')]) - replicant_list.append(replicant_table) - table.add_row(['Replicant Volumes', replicant_list]) + 'Schedule', utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname')]) + table.add_row(['Replicant Volumes', replicant_table]) if file_volume.get('originalVolumeSize'): original_volume_info = formatting.Table(['Property', 'Value']) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index b8c6e4a8f..c4c284636 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -14,6 +14,7 @@ import click from rich import box +from rich.errors import NotRenderableError from rich.table import Table as rTable from SoftLayer.CLI import exceptions @@ -392,7 +393,15 @@ def prettytable(self, fmt='table', theme=None): table.add_column(col, justify=justify, style=style) for row in self.rows: - table.add_row(*row) + try: + table.add_row(*row) + # Generally you will see this if one of the columns in the row is a list or dict + except NotRenderableError: + forced_row = [] + for i in row: + forced_row.append(str(i)) + table.add_row(*forced_row) + return table diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py index 2d9cdc499..d9e927bd8 100644 --- a/SoftLayer/fixtures/SoftLayer_Network_Storage.py +++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py @@ -334,4 +334,107 @@ 'storageType': { 'keyName': 'ENDURANCE_BLOCK_STORAGE' } +} + + +FILE_DETAIL_ISSUE2154 = { + "capacityGb": 150, + "id": 609491933, + "username": "SL02SV1414935_187", + "activeTransactionCount": 0, + "replicationPartnerCount": 1, + "fileNetworkMountAddress": "fsf-natestdal0505-fcb-fz.service.softlayer.com:/SL02SV1414935_187/data01", + "originalVolumeSize": "20", + "provisionedIops": "2000", + "replicationStatus": "FAILOVER_COMPLETED", + "serviceResourceBackendIpAddress": "fsf-natestdal0505-fcb-fz.service.softlayer.com", + "snapshotCapacityGb": "5", + "activeTransactions": [ + { + "createDate": "", + "elapsedSeconds": 111763, + "guestId": "", + "hardwareId": "", + "id": "", + "modifyDate": "", + "statusChangeDate": "", + "transactionGroup": { + "name": "Volume Modification" + }, + "transactionStatus": { + "friendlyName": "In Progress" + } + } + ], + "parentVolume": { + "accountId": 1414935, + "capacityGb": 120, + "createDate": "2024-05-16T02:28:02-05:00", + "guestId": "", + "hardwareId": "", + "hostId": "", + "id": 609491967, + "nasType": "SNAPSHOT", + "notes": "vol_duplicate_snapshot_2024-05-16_0228", + "serviceProviderId": 1, + "storageTypeId": "16", + "upgradableFlag": True, + "username": "SL02SV1414935_187", + "serviceResourceBackendIpAddress": "fsf-natestdal0505-fcb-fz.service.softlayer.com", + "serviceResourceName": "Storage Type 02 Aggregate natestdal0505-fc-d", + "snapshotSizeBytes": "0" + }, + "replicationPartners": [ + { + "id": 609491945, + "username": "SL02SV1414935_187_REP_1", + "serviceResourceBackendIpAddress": "fsf-natestdal0505-ffb-fz.service.softlayer.com", + "replicationSchedule": { + "active": 1, + "createDate": "2024-05-16T01:20:19-05:00", + "id": 666339, + "modifyDate": "", + "name": "SL02SV1414935_187_HOURLY_REP", + "partnershipId": "", + "typeId": 32, + "volumeId": 609491933, + "type": { + "keyname": "REPLICATION_HOURLY" + } + }, + "serviceResource": { + "backendIpAddress": "fsf-natestdal0505-ffb-fz.service.softlayer.com", + "id": 57365, + "name": "Storage Type 02 Aggregate natestdal0505-ff-d", + "datacenter": { + "name": "dal10" + }, + "type": { + "type": "NETAPP_STOR_AGGR" + } + } + } + ], + "serviceResource": { + "backendIpAddress": "fsf-natestdal0505-fcb-fz.service.softlayer.com", + "id": 52292, + "name": "Storage Type 02 Aggregate natestdal0505-fc-d", + "attributes": [ + { + "value": "2", + "attributeType": { + "keyname": "STAAS_VERSION" + } + } + ], + "datacenter": { + "name": "lon02" + }, + "type": { + "type": "NETAPP_STOR_AGGR" + } + }, + "storageType": { + "keyName": "PERFORMANCE_FILE_STORAGE" } +} diff --git a/tests/CLI/formatting_table_tests.py b/tests/CLI/formatting_table_tests.py index 117667072..4d62a742b 100644 --- a/tests/CLI/formatting_table_tests.py +++ b/tests/CLI/formatting_table_tests.py @@ -4,6 +4,8 @@ :license: MIT, see LICENSE for more details. """ +from rich.console import Console + from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting @@ -21,6 +23,48 @@ def test_boolean_table(self): table.add_row(["entry1"]) self.assertTrue(table) + def test_key_value_table(self): + expected = """┌───────────┬─────────────────────────┐ +│ Key │ Value │ +├───────────┼─────────────────────────┤ +│ First │ One │ +│ Sub Table │ ┌─────────┬───────────┐ │ +│ │ │ Sub Key │ Sub Value │ │ +│ │ ├─────────┼───────────┤ │ +│ │ │ Second │ Two │ │ +│ │ └─────────┴───────────┘ │ +└───────────┴─────────────────────────┘ +""" + table = formatting.KeyValueTable(["Key", "Value"]) + table.add_row(["First", "One"]) + sub_table = formatting.KeyValueTable(["Sub Key", "Sub Value"]) + sub_table.add_row(["Second", "Two"]) + table.add_row(["Sub Table", sub_table]) + console = Console() + + with console.capture() as capture: + to_print = formatting.format_output(table) + console.print(to_print) + result = capture.get() + self.assertEqual(expected, result) + + def test_unrenderable_recovery_table(self): + expected = """│ Sub Table │ [ Date: Tue, 28 May 2024 18:10:54 -0500 Subject: [PATCH 06/77] Fixed up some unit tests --- SoftLayer/CLI/block/detail.py | 59 +++------ SoftLayer/CLI/exceptions.py | 15 ++- SoftLayer/CLI/file/detail.py | 19 ++- SoftLayer/testing/__init__.py | 1 + tests/CLI/modules/block_tests.py | 52 ++++---- tests/CLI/modules/file_tests.py | 197 ++++++++++++------------------- 6 files changed, 145 insertions(+), 198 deletions(-) diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py index 263e21b48..a4359fae3 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -31,72 +31,51 @@ def cli(env, volume_id): table.add_row(['Username', block_volume['username']]) table.add_row(['Type', storage_type]) table.add_row(['Capacity (GB)', capacity]) - table.add_row(['LUN Id', "%s" % block_volume['lunId']]) + table.add_row(['LUN Id', block_volume['lunId']]) if block_volume.get('provisionedIops'): - table.add_row(['IOPs', float(block_volume['provisionedIops'])]) + table.add_row(['IOPs', block_volume['provisionedIops']]) if block_volume.get('storageTierLevel'): - table.add_row([ - 'Endurance Tier', - block_volume['storageTierLevel'], - ]) - - table.add_row([ - 'Data Center', - block_volume['serviceResource']['datacenter']['name'], - ]) - table.add_row([ - 'Target IP', - block_volume['serviceResourceBackendIpAddress'], - ]) + table.add_row(['Endurance Tier', block_volume['storageTierLevel']]) + + table.add_row(['Data Center', block_volume['serviceResource']['datacenter']['name']]) + table.add_row(['Target IP', block_volume['serviceResourceBackendIpAddress']]) if block_volume['snapshotCapacityGb']: - table.add_row([ - 'Snapshot Capacity (GB)', - block_volume['snapshotCapacityGb'], - ]) + table.add_row(['Snapshot Capacity (GB)', block_volume['snapshotCapacityGb']]) if 'snapshotSizeBytes' in block_volume['parentVolume']: - table.add_row([ - 'Snapshot Used (Bytes)', - block_volume['parentVolume']['snapshotSizeBytes'], - ]) + table.add_row(['Snapshot Used (Bytes)', block_volume['parentVolume']['snapshotSizeBytes']]) - table.add_row(['# of Active Transactions', "%i" - % block_volume['activeTransactionCount']]) + table.add_row(['# of Active Transactions', block_volume['activeTransactionCount']]) if block_volume['activeTransactions']: for trans in block_volume['activeTransactions']: if 'transactionStatus' in trans and 'friendlyName' in trans['transactionStatus']: table.add_row(['Ongoing Transaction', trans['transactionStatus']['friendlyName']]) - table.add_row(['Replicant Count', "%u" % block_volume.get('replicationPartnerCount', 0)]) + table.add_row(['Replicant Count', block_volume.get('replicationPartnerCount', 0)]) if block_volume['replicationPartnerCount'] > 0: # This if/else temporarily handles a bug in which the SL API # returns a string or object for 'replicationStatus'; it seems that # the type is string for File volumes and object for Block volumes if 'message' in block_volume['replicationStatus']: - table.add_row(['Replication Status', "%s" - % block_volume['replicationStatus']['message']]) + table.add_row(['Replication Status', block_volume['replicationStatus']['message']]) else: - table.add_row(['Replication Status', "%s" - % block_volume['replicationStatus']]) + table.add_row(['Replication Status', block_volume['replicationStatus']]) - replicant_table = formatting.KeyValueTable(['Name', 'Value']) + replicant_table = formatting.Table(['Id', 'Username', 'Target', 'Location', 'Schedule']) replicant_table.align['Name'] = 'r' replicant_table.align['Value'] = 'l' for replicant in block_volume['replicationPartners']: replicant_table.add_row([ - 'Replicant Id', replicant['id']]) - replicant_table.add_row([ - 'Volume Name', utils.lookup(replicant, 'username')]) - replicant_table.add_row([ - 'Target IP', utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) - replicant_table.add_row([ - 'Data Center', utils.lookup(replicant, 'serviceResource', 'datacenter', 'name')]) - replicant_table.add_row([ - 'Schedule', utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname')]) + replicant.get('id'), + utils.lookup(replicant, 'username'), + utils.lookup(replicant, 'serviceResourceBackendIpAddress'), + utils.lookup(replicant, 'serviceResource', 'datacenter', 'name'), + utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname') + ]) table.add_row(['Replicant Volumes', replicant_table]) if block_volume.get('originalVolumeSize'): diff --git a/SoftLayer/CLI/exceptions.py b/SoftLayer/CLI/exceptions.py index 611d854ea..55acdb60e 100644 --- a/SoftLayer/CLI/exceptions.py +++ b/SoftLayer/CLI/exceptions.py @@ -16,9 +16,8 @@ def __init__(self, code=0, *args): self.code = code def __str__(self): - return "" % (self.code, - getattr(self, 'message')) - + message = getattr(self, 'message') + return f"" __repr__ = __str__ @@ -29,6 +28,11 @@ def __init__(self, msg, *args): super().__init__(code=2, *args) self.message = msg + def __str__(self): + message = getattr(self, 'message') + return f"" + __repr__ = __str__ + class ArgumentError(CLIAbort): """Halt the execution of the command because of invalid arguments.""" @@ -36,3 +40,8 @@ class ArgumentError(CLIAbort): def __init__(self, msg, *args): super().__init__(msg, *args) self.message = "Argument Error: %s" % msg + + def __str__(self): + message = getattr(self, 'message') + return f"" + __repr__ = __str__ diff --git a/SoftLayer/CLI/file/detail.py b/SoftLayer/CLI/file/detail.py index 009dc1fc9..ca12e43a3 100644 --- a/SoftLayer/CLI/file/detail.py +++ b/SoftLayer/CLI/file/detail.py @@ -41,7 +41,7 @@ def cli(env, volume_id): table.add_row(['Used Space', used_space]) if file_volume.get('provisionedIops'): - table.add_row(['IOPs', float(file_volume['provisionedIops'])]) + table.add_row(['IOPs', file_volume['provisionedIops']]) if file_volume.get('storageTierLevel'): table.add_row(['Endurance Tier', file_volume['storageTierLevel']]) @@ -75,20 +75,17 @@ def cli(env, volume_id): else: table.add_row(['Replication Status', file_volume['replicationStatus']]) - replicant_table = formatting.KeyValueTable(['Name', 'Value']) + replicant_table = formatting.Table(['Id', 'Username', 'Target', 'Location', 'Schedule']) replicant_table.align['Name'] = 'r' replicant_table.align['Value'] = 'l' for replicant in file_volume['replicationPartners']: replicant_table.add_row([ - 'Volume ID', replicant.get('id')]) - replicant_table.add_row([ - 'Volume Name', utils.lookup(replicant, 'username')]) - replicant_table.add_row([ - 'Target IP', utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) - replicant_table.add_row([ - 'Data Center', utils.lookup(replicant, 'serviceResource', 'datacenter', 'name')]) - replicant_table.add_row([ - 'Schedule', utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname')]) + replicant.get('id'), + utils.lookup(replicant, 'username'), + utils.lookup(replicant, 'serviceResourceBackendIpAddress'), + utils.lookup(replicant, 'serviceResource', 'datacenter', 'name'), + utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname') + ]) table.add_row(['Replicant Volumes', replicant_table]) if file_volume.get('originalVolumeSize'): diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py index 6eff9851c..b55ea2e86 100644 --- a/SoftLayer/testing/__init__.py +++ b/SoftLayer/testing/__init__.py @@ -111,6 +111,7 @@ def setUp(self): # NOQA self.env = environment.Environment() self.env.client = self.client self.set_up() + self.maxDiff = None def tearDown(self): # NOQA super().tearDown() diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py index 5a7cd7c84..d21c8dece 100644 --- a/tests/CLI/modules/block_tests.py +++ b/tests/CLI/modules/block_tests.py @@ -65,10 +65,9 @@ def test_volume_detail(self): self.assert_called_with('SoftLayer_Network_Storage', 'getObject', identifier=1234) self.assertEqual({ 'Username': 'username', - 'LUN Id': '2', - 'Notes': "{'status': 'available'}", 'Endurance Tier': 'READHEAVY_TIER', - 'IOPs': 1000.0, + 'IOPs': "1000", + 'LUN Id': 2, 'Snapshot Capacity (GB)': '10', 'Snapshot Used (Bytes)': 1024, 'Capacity (GB)': '20GB', @@ -76,24 +75,31 @@ def test_volume_detail(self): 'Data Center': 'dal05', 'Type': 'ENDURANCE', 'ID': 100, - '# of Active Transactions': '1', + 'Notes': "{'status': 'available'}", + '# of Active Transactions': 1, 'Ongoing Transaction': 'This is a buffer time in which the customer may cancel the server', - 'Replicant Count': '1', - 'Replication Status': 'Replicant Volume Provisioning ' - 'has completed.', - 'Replicant Volumes': [ - {'Name': 'Replicant Id', 'Value': 1785}, - {'Name': 'Volume Name', 'Value': 'TEST_REP_2'}, - {'Name': 'Target IP', 'Value': '10.3.177.84'}, - {'Name': 'Data Center', 'Value': 'dal01'}, - {'Name': 'Schedule', 'Value': 'REPLICATION_DAILY'}], + 'Replicant Count': 1, + 'Replication Status': 'Replicant Volume Provisioning has completed.', + "Replicant Volumes": [ + { + "Id": 1784, + "Username": "TEST_REP_1", + "Target": "10.3.174.79", + "Location": "wdc01", + "Schedule": "REPLICATION_HOURLY" + }, + { + "Id": 1785, + "Username": "TEST_REP_2", + "Target": "10.3.177.84", + "Location": "dal01", + "Schedule": "REPLICATION_DAILY" + } + ], 'Original Volume Properties': [ - {'Property': 'Original Volume Size', - 'Value': '20'}, - {'Property': 'Original Volume Name', - 'Value': 'test-original-volume-name'}, - {'Property': 'Original Snapshot Name', - 'Value': 'test-original-snapshot-name'} + {'Property': 'Original Volume Size', 'Value': '20'}, + {'Property': 'Original Volume Name', 'Value': 'test-original-volume-name'}, + {'Property': 'Original Snapshot Name', 'Value': 'test-original-snapshot-name'} ] }, json.loads(result.output)) @@ -102,10 +108,10 @@ def test_block_detail_issue1732(self): lun_mock.return_value = SoftLayer_Network_Storage.BLOCK_LIST_ISSUES_1732 result = self.run_command(['--format=table', 'block', 'volume-detail', '1234']) self.assert_no_fail(result) - self.assertIn('│ Username │ SL02SEL307608-60 │', result.output) - self.assertIn('│ Capacity (GB) │ 16000GB │', result.output) - self.assertIn('│ Replication Status │ FAILBACK_COMPLETED │', result.output) - self.assertIn('│ Notes │ test │', result.output) + self.assertIn('Username │ SL02SEL307608-60', result.output) + self.assertIn('Capacity (GB) │ 16000GB', result.output) + self.assertIn('Replication Status │ FAILBACK_COMPLETED', result.output) + self.assertIn('Notes │ test', result.output) def test_volume_detail_name_identifier(self): result = self.run_command(['block', 'volume-detail', 'SL-12345']) diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py index 19fc7ffe2..c68ac7a08 100644 --- a/tests/CLI/modules/file_tests.py +++ b/tests/CLI/modules/file_tests.py @@ -163,11 +163,12 @@ def test_volume_detail(self): result = self.run_command(['file', 'volume-detail', '1234']) self.assert_no_fail(result) + print(result.output) self.assertEqual({ 'Username': 'username', - 'Used Space': '0B', + 'Used Space': '0.00 MB', 'Endurance Tier': 'READHEAVY_TIER', - 'IOPs': 1000, + 'IOPs': "1000", 'Mount Address': '127.0.0.1:/TEST', 'Snapshot Capacity (GB)': '10', 'Snapshot Used (Bytes)': 1024, @@ -177,29 +178,30 @@ def test_volume_detail(self): 'Type': 'ENDURANCE', 'ID': 100, 'Notes': "{'status': 'available'}", - '# of Active Transactions': '1', + '# of Active Transactions': 1, 'Ongoing Transaction': 'This is a buffer time in which the customer may cancel the server', 'Replicant Count': '1', - 'Replication Status': 'Replicant Volume Provisioning ' - 'has completed.', - 'Replicant Volumes': [[ - {'Replicant ID': 'Volume Name', '1784': 'TEST_REP_1'}, - {'Replicant ID': 'Target IP', '1784': '10.3.174.79'}, - {'Replicant ID': 'Data Center', '1784': 'wdc01'}, - {'Replicant ID': 'Schedule', '1784': 'REPLICATION_HOURLY'}, - ], [ - {'Replicant ID': 'Volume Name', '1785': 'TEST_REP_2'}, - {'Replicant ID': 'Target IP', '1785': '10.3.177.84'}, - {'Replicant ID': 'Data Center', '1785': 'dal01'}, - {'Replicant ID': 'Schedule', '1785': 'REPLICATION_DAILY'}, - ]], + 'Replication Status': 'Replicant Volume Provisioning has completed.', + "Replicant Volumes": [ + { + "Id": 1784, + "Username": "TEST_REP_1", + "Target": "10.3.174.79", + "Location": "wdc01", + "Schedule": "REPLICATION_HOURLY" + }, + { + "Id": 1785, + "Username": "TEST_REP_2", + "Target": "10.3.177.84", + "Location": "dal01", + "Schedule": "REPLICATION_DAILY" + } + ], 'Original Volume Properties': [ - {'Property': 'Original Volume Size', - 'Value': '20'}, - {'Property': 'Original Volume Name', - 'Value': 'test-original-volume-name'}, - {'Property': 'Original Snapshot Name', - 'Value': 'test-original-snapshot-name'} + {'Property': 'Original Volume Size', 'Value': '20'}, + {'Property': 'Original Volume Name', 'Value': 'test-original-volume-name'}, + {'Property': 'Original Snapshot Name', 'Value': 'test-original-snapshot-name'} ] }, json.loads(result.output)) @@ -229,17 +231,14 @@ def test_volume_detail_issues2154(self): self.assertIn("SL02SV1414935_187", result.output) def test_volume_order_performance_iops_not_given(self): - result = self.run_command(['file', 'volume-order', - '--storage-type=performance', '--size=20', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=performance', '--size=20', '--location=dal05']) self.assertEqual(2, result.exit_code) def test_volume_order_performance_snapshot_error(self): - result = self.run_command(['file', 'volume-order', - '--storage-type=performance', '--size=20', - '--iops=100', '--location=dal05', - '--snapshot-size=10', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=performance', '--size=20', + '--iops=100', '--location=dal05', '--snapshot-size=10', '--service-offering=performance']) self.assertEqual(2, result.exit_code) @@ -258,10 +257,8 @@ def test_volume_order_performance(self, order_mock): } } - result = self.run_command(['file', 'volume-order', - '--storage-type=performance', '--size=20', - '--iops=100', '--location=dal05', - '--snapshot-size=10']) + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=performance', '--size=20', + '--iops=100', '--location=dal05', '--snapshot-size=10']) self.assert_no_fail(result) self.assertEqual(result.output, @@ -273,8 +270,7 @@ def test_volume_order_performance(self, order_mock): 'ready.\n') def test_volume_order_endurance_tier_not_given(self): - result = self.run_command(['file', 'volume-order', - '--storage-type=endurance', '--size=20', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=endurance', '--size=20', '--location=dal05']) self.assertEqual(2, result.exit_code) @@ -293,10 +289,8 @@ def test_volume_order_endurance(self, order_mock): } } - result = self.run_command(['file', 'volume-order', - '--storage-type=endurance', '--size=20', - '--tier=0.25', '--location=dal05', - '--snapshot-size=10']) + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=endurance', '--size=20', + '--tier=0.25', '--location=dal05', '--snapshot-size=10']) self.assert_no_fail(result) self.assertEqual(result.output, @@ -311,20 +305,17 @@ def test_volume_order_endurance(self, order_mock): def test_volume_order_order_not_placed(self, order_mock): order_mock.return_value = {} - result = self.run_command(['file', 'volume-order', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=endurance', '--size=20', '--tier=0.25', '--location=dal05']) self.assert_no_fail(result) self.assertEqual(result.output, - 'Order could not be placed! Please verify ' - 'your options and try again.\n') + 'Order could not be placed! Please verify your options and try again.\n') def test_volume_order_hourly_billing_not_available(self): - result = self.run_command(['file', 'volume-order', - '--storage-type=endurance', '--size=20', - '--tier=0.25', '--location=dal10', - '--billing=hourly', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=endurance', '--size=20', + '--tier=0.25', '--location=dal10', '--billing=hourly', '--service-offering=enterprise']) self.assertEqual(2, result.exit_code) @@ -343,10 +334,8 @@ def test_volume_order_hourly_billing(self, order_mock): } } - result = self.run_command(['file', 'volume-order', - '--storage-type=endurance', '--size=20', - '--tier=0.25', '--location=dal05', - '--service-offering=storage_as_a_service', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=endurance', '--size=20', + '--tier=0.25', '--location=dal05', '--service-offering=storage_as_a_service', '--billing=hourly', '--snapshot-size=10']) self.assert_no_fail(result) @@ -364,39 +353,35 @@ def test_volume_order_hourly_billing(self, order_mock): def test_volume_order_performance_manager_error(self, order_mock): order_mock.side_effect = ValueError('failure!') - result = self.run_command(['file', 'volume-order', - '--storage-type=performance', '--size=20', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=performance', '--size=20', '--iops=100', '--location=dal05']) self.assertEqual(2, result.exit_code) + print(result.output) self.assertEqual('Argument Error: failure!', result.exception.message) @mock.patch('SoftLayer.FileStorageManager.order_file_volume') def test_volume_order_endurance_manager_error(self, order_mock): order_mock.side_effect = ValueError('failure!') - result = self.run_command(['file', 'volume-order', - '--storage-type=endurance', '--size=20', + result = self.run_command(['--really', 'file', 'volume-order', '--storage-type=endurance', '--size=20', '--tier=0.25', '--location=dal05']) self.assertEqual(2, result.exit_code) self.assertEqual('Argument Error: failure!', result.exception.message) def test_enable_snapshots(self): - result = self.run_command(['file', 'snapshot-enable', '12345678', - '--schedule-type=HOURLY', '--minute=10', + result = self.run_command(['file', 'snapshot-enable', '12345678', '--schedule-type=HOURLY', '--minute=10', '--retention-count=5']) self.assert_no_fail(result) def test_disable_snapshots(self): - result = self.run_command(['file', 'snapshot-disable', '12345678', - '--schedule-type=HOURLY']) + result = self.run_command(['file', 'snapshot-disable', '12345678', '--schedule-type=HOURLY']) self.assert_no_fail(result) def test_list_volume_schedules(self): - result = self.run_command([ - 'file', 'snapshot-schedule-list', '12345678']) + result = self.run_command(['file', 'snapshot-schedule-list', '12345678']) self.assert_no_fail(result) self.assertEqual([ { @@ -465,20 +450,17 @@ def test_delete_snapshot(self): def test_snapshot_order_order_not_placed(self, order_mock): order_mock.return_value = {} - result = self.run_command(['file', 'snapshot-order', '1234', - '--capacity=10', '--tier=0.25']) + result = self.run_command(['--really', 'file', 'snapshot-order', '1234', '--capacity=10', '--tier=0.25']) self.assert_no_fail(result) self.assertEqual(result.output, - 'Order could not be placed! Please verify ' - 'your options and try again.\n') + 'Order could not be placed! Please verify your options and try again.\n') @mock.patch('SoftLayer.FileStorageManager.order_snapshot_space') def test_snapshot_order_performance_manager_error(self, order_mock): order_mock.side_effect = ValueError('failure!') - result = self.run_command(['file', 'snapshot-order', '1234', - '--capacity=10', '--tier=0.25']) + result = self.run_command(['--really', 'file', 'snapshot-order', '1234', '--capacity=10', '--tier=0.25']) self.assertEqual(2, result.exit_code) self.assertEqual('Argument Error: failure!', result.exception.message) @@ -494,8 +476,7 @@ def test_snapshot_order(self, order_mock): } } - result = self.run_command(['file', 'snapshot-order', '1234', - '--capacity=10', '--tier=0.25']) + result = self.run_command(['--really', 'file', 'snapshot-order', '1234', '--capacity=10', '--tier=0.25']) self.assert_no_fail(result) self.assertEqual(result.output, @@ -504,32 +485,25 @@ def test_snapshot_order(self, order_mock): ' > Order status: PENDING_APPROVAL\n') def test_snapshot_cancel(self): - result = self.run_command(['--really', - 'file', 'snapshot-cancel', '1234']) + result = self.run_command(['--really', 'file', 'snapshot-cancel', '1234']) self.assert_no_fail(result) - self.assertEqual('File volume with id 1234 has been marked' - ' for snapshot cancellation\n', result.output) - self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', - args=(False, True, None)) + self.assertEqual('File volume with id 1234 has been marked for snapshot cancellation\n', result.output) + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', args=(False, True, None)) def test_replicant_failover(self): - result = self.run_command(['file', 'replica-failover', '12345678', - '--replicant-id=5678']) + result = self.run_command(['file', 'replica-failover', '12345678', '--replicant-id=5678']) self.assert_no_fail(result) - self.assertEqual('Failover to replicant is now in progress.\n', - result.output) + self.assertEqual('Failover to replicant is now in progress.\n', result.output) @mock.patch('SoftLayer.FileStorageManager.failover_to_replicant') def test_replicant_failover_unsuccessful(self, failover_mock): failover_mock.return_value = False - result = self.run_command(['file', 'replica-failover', '12345678', - '--replicant-id=5678']) + result = self.run_command(['file', 'replica-failover', '12345678', '--replicant-id=5678']) - self.assertEqual('Failover operation could not be initiated.\n', - result.output) + self.assertEqual('Failover operation could not be initiated.\n', result.output) @mock.patch('SoftLayer.CLI.formatting.confirm') @mock.patch('SoftLayer.FileStorageManager.disaster_recovery_failover_to_replicant') @@ -545,8 +519,7 @@ def test_disaster_recovery_failover(self, disaster_recovery_failover_mock, confi def test_disaster_recovery_failover_aborted(self, confirm_mock): confirm_mock.return_value = False - result = self.run_command(['file', 'disaster-recovery-failover', '12345678', - '--replicant-id=5678']) + result = self.run_command(['file', 'disaster-recovery-failover', '12345678', '--replicant-id=5678']) self.assertEqual(result.exit_code, 2) self.assertIsInstance(result.exception, exceptions.CLIAbort) @@ -555,8 +528,7 @@ def test_replicant_failback(self): result = self.run_command(['file', 'replica-failback', '12345678']) self.assert_no_fail(result) - self.assertEqual('Failback from replicant is now in progress.\n', - result.output) + self.assertEqual('Failback from replicant is now in progress.\n', result.output) @mock.patch('SoftLayer.FileStorageManager.failback_from_replicant') def test_replicant_failback_unsuccessful(self, failback_mock): @@ -564,21 +536,18 @@ def test_replicant_failback_unsuccessful(self, failback_mock): result = self.run_command(['file', 'replica-failback', '12345678']) - self.assertEqual('Failback operation could not be initiated.\n', - result.output) + self.assertEqual('Failback operation could not be initiated.\n', result.output) @mock.patch('SoftLayer.FileStorageManager.order_replicant_volume') def test_replicant_order_order_not_placed(self, order_mock): order_mock.return_value = {} - result = self.run_command(['file', 'replica-order', '100', - '--snapshot-schedule=DAILY', + result = self.run_command(['--really', 'file', 'replica-order', '100', '--snapshot-schedule=DAILY', '--location=dal05']) self.assert_no_fail(result) self.assertEqual(result.output, - 'Order could not be placed! Please verify ' - 'your options and try again.\n') + 'Order could not be placed! Please verify your options and try again.\n') @mock.patch('SoftLayer.FileStorageManager.order_replicant_volume') def test_replicant_order(self, order_mock): @@ -596,9 +565,8 @@ def test_replicant_order(self, order_mock): } } - result = self.run_command(['file', 'replica-order', '100', - '--snapshot-schedule=DAILY', - '--location=dal05', '--tier=2']) + result = self.run_command(['--really', 'file', 'replica-order', '100', + '--snapshot-schedule=DAILY', '--location=dal05', '--tier=2']) self.assert_no_fail(result) self.assertEqual(result.output, @@ -613,19 +581,14 @@ def test_replicant_order(self, order_mock): def test_replication_locations(self): result = self.run_command(['file', 'replica-locations', '1234']) self.assert_no_fail(result) - self.assertEqual( - { - '12345': 'Dallas 05', - }, - json.loads(result.output)) + self.assertEqual({'12345': 'Dallas 05'}, json.loads(result.output)) @mock.patch('SoftLayer.FileStorageManager.get_replication_locations') def test_replication_locations_unsuccessful(self, locations_mock): locations_mock.return_value = False result = self.run_command(['file', 'replica-locations', '1234']) self.assert_no_fail(result) - self.assertEqual('No data centers compatible for replication.\n', - result.output) + self.assertEqual('No data centers compatible for replication.\n', result.output) def test_replication_partners(self): result = self.run_command(['file', 'replica-partners', '1234']) @@ -663,23 +626,19 @@ def test_replication_partners_unsuccessful(self, partners_mock): def test_duplicate_order_exception_caught(self, order_mock): order_mock.side_effect = ValueError('order attempt failed, oh noooo!') - result = self.run_command(['file', 'volume-duplicate', '100']) + result = self.run_command(['--really', 'file', 'volume-duplicate', '100']) self.assertEqual(2, result.exit_code) - self.assertEqual('Argument Error: order attempt failed, oh noooo!', - result.exception.message) + self.assertEqual('Argument Error: order attempt failed, oh noooo!', result.exception.message) @mock.patch('SoftLayer.FileStorageManager.order_duplicate_volume') def test_duplicate_order_order_not_placed(self, order_mock): order_mock.return_value = {} - result = self.run_command(['file', 'volume-duplicate', '100', - '--duplicate-iops=1400']) + result = self.run_command(['--really', 'file', 'volume-duplicate', '100', '--duplicate-iops=1400']) self.assert_no_fail(result) - self.assertEqual(result.output, - 'Order could not be placed! Please verify ' - 'your options and try again.\n') + self.assertEqual(result.output, 'Order could not be placed! Please verify your options and try again.\n') @mock.patch('SoftLayer.FileStorageManager.order_duplicate_volume') def test_duplicate_order(self, order_mock): @@ -690,11 +649,9 @@ def test_duplicate_order(self, order_mock): } } - result = self.run_command(['file', 'volume-duplicate', '100', - '--origin-snapshot-id=470', - '--duplicate-size=250', - '--duplicate-tier=2', - '--duplicate-snapshot-size=20']) + result = self.run_command(['--really', 'file', 'volume-duplicate', '100', + '--origin-snapshot-id=470', '--duplicate-size=250', + '--duplicate-tier=2', '--duplicate-snapshot-size=20']) self.assert_no_fail(result) self.assertEqual(result.output, @@ -710,10 +667,8 @@ def test_duplicate_order_hourly_billing(self, order_mock): } } - result = self.run_command(['file', 'volume-duplicate', '100', - '--origin-snapshot-id=470', - '--duplicate-size=250', - '--duplicate-tier=2', '--billing=hourly', + result = self.run_command(['--really', 'file', 'volume-duplicate', '100', '--origin-snapshot-id=470', + '--duplicate-size=250', '--duplicate-tier=2', '--billing=hourly', '--duplicate-snapshot-size=20']) order_mock.assert_called_with('100', origin_snapshot_id=470, @@ -731,7 +686,7 @@ def test_duplicate_order_hourly_billing(self, order_mock): def test_modify_order_exception_caught(self, order_mock): order_mock.side_effect = ValueError('order attempt failed, noooo!') - result = self.run_command(['file', 'volume-modify', '102', '--new-size=1000']) + result = self.run_command(['--really', 'file', 'volume-modify', '102', '--new-size=1000']) self.assertEqual(2, result.exit_code) self.assertEqual('Argument Error: order attempt failed, noooo!', result.exception.message) @@ -740,7 +695,7 @@ def test_modify_order_exception_caught(self, order_mock): def test_modify_order_order_not_placed(self, order_mock): order_mock.return_value = {} - result = self.run_command(['file', 'volume-modify', '102', '--new-iops=1400']) + result = self.run_command(['--really', 'file', 'volume-modify', '102', '--new-iops=1400']) self.assert_no_fail(result) self.assertEqual('Order could not be placed! Please verify your options and try again.\n', result.output) @@ -751,7 +706,7 @@ def test_modify_order(self, order_mock): {'description': '1000 GBs'}, {'description': '4 IOPS per GB'}]}} - result = self.run_command(['file', 'volume-modify', '102', '--new-size=1000', '--new-tier=4']) + result = self.run_command(['--really', 'file', 'volume-modify', '102', '--new-size=1000', '--new-tier=4']) order_mock.assert_called_with('102', new_size=1000, new_iops=None, new_tier_level=4) self.assert_no_fail(result) From ed42da56ba9b8968c9d23ae34973b5de70dd1d3f Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 6 Jun 2024 15:51:30 -0500 Subject: [PATCH 07/77] Reworked user permissions list to include permissions by department and be more accurate with new permissions being added #2156 --- SoftLayer/CLI/user/permissions.py | 29 +- .../SoftLayer_User_Permission_Department.py | 978 ++++++++++++++++++ SoftLayer/managers/user.py | 16 +- tests/CLI/modules/user_tests.py | 2 +- tests/managers/user_tests.py | 7 + 5 files changed, 1018 insertions(+), 14 deletions(-) create mode 100644 SoftLayer/fixtures/SoftLayer_User_Permission_Department.py diff --git a/SoftLayer/CLI/user/permissions.py b/SoftLayer/CLI/user/permissions.py index 4aaeeb12e..7f8fd06eb 100644 --- a/SoftLayer/CLI/user/permissions.py +++ b/SoftLayer/CLI/user/permissions.py @@ -1,4 +1,4 @@ -"""List A users permissions.""" +"""List a users permissions.""" import click import SoftLayer @@ -11,21 +11,31 @@ @click.argument('identifier') @environment.pass_env def cli(env, identifier): - """User Permissions.""" + """User Permissions. + + Some permissions here may also be managed by IBM IAM service. + See https://cloud.ibm.com/docs/account?topic=account-migrated_permissions for more details. + """ mgr = SoftLayer.UserManager(env.client) user_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'username') object_mask = "mask[id, permissions, isMasterUserFlag, roles]" user = mgr.get_user(user_id, object_mask) - all_permissions = mgr.get_all_permissions() - user_permissions = perms_to_dict(user['permissions']) + all_permissions = mgr.get_permission_departments() + user_permissions = perms_to_dict(user['permissions']) + all_table = formatting.KeyValueTable(['Department', 'Permissions']) if user['isMasterUserFlag']: click.secho('This account is the Master User and has all permissions enabled', fg='green') env.fout(roles_table(user)) - env.fout(permission_table(user_permissions, all_permissions)) + for department in all_permissions: + all_table.add_row([ + department.get('name'), + permission_table(user_permissions, department.get('permissions', [])) + ]) + env.fout(all_table) def perms_to_dict(perms): @@ -39,18 +49,13 @@ def perms_to_dict(perms): def permission_table(user_permissions, all_permissions): """Creates a table of available permissions""" - table = formatting.Table(['Description', 'KeyName', 'Assigned']) + table = formatting.Table(['KeyName', 'Assigned', 'Description']) table.align['KeyName'] = 'l' table.align['Description'] = 'l' table.align['Assigned'] = 'l' for perm in all_permissions: assigned = user_permissions.get(perm['keyName'], False) - hide_permission_list = ['ACCOUNT_SUMMARY_VIEW', 'REQUEST_COMPLIANCE_REPORT', - 'COMPANY_EDIT', 'ONE_TIME_PAYMENTS', 'UPDATE_PAYMENT_DETAILS', - 'EU_LIMITED_PROCESSING_MANAGE', 'TICKET_ADD', 'TICKET_EDIT', - 'TICKET_SEARCH', 'TICKET_VIEW', 'TICKET_VIEW_ALL'] - if perm['keyName'] not in hide_permission_list: - table.add_row([perm['name'], perm['keyName'], assigned]) + table.add_row([perm['keyName'], assigned, perm['description']]) return table diff --git a/SoftLayer/fixtures/SoftLayer_User_Permission_Department.py b/SoftLayer/fixtures/SoftLayer_User_Permission_Department.py new file mode 100644 index 000000000..0510ff2a4 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_User_Permission_Department.py @@ -0,0 +1,978 @@ +getAllObjects = [ + { + "description": "Administrative", + "id": 1, + "keyName": "ADMINISTRATIVE", + "name": "Administrative", + "permissions": [ + { + "createDate": None, + "departmentId": 1, + "description": "Permission to access account billing system type determination endpoint", + "id": 5088, + "key": None, + "keyName": "ACCOUNT_BILLING_SYSTEM", + "modifyDate": None, + "name": "Account Billing System" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Activate Partner Customer Account", + "id": 1315, + "key": "A_16", + "keyName": "ACTIVATE_PARTNER_ACCOUNT", + "modifyDate": None, + "name": "Activate Partner Customer Account" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Permission to create sub brands", + "id": 1313, + "key": "A_13", + "keyName": "ACCOUNT_BRAND_ADD", + "modifyDate": None, + "name": "Add Brand Account" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Permission to create customer account.", + "id": 1312, + "key": "A_14", + "keyName": "ACCOUNT_CUSTOMER_ADD", + "modifyDate": None, + "name": "Add Customer Account" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Permission to interface with the Automated Brand Migration process", + "id": 5089, + "key": None, + "keyName": "AUTOMATED_BRAND_MIGRATION", + "modifyDate": None, + "name": "Automated Brand Migration" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Modify the account and company profile associated with this account.", + "id": 1268, + "key": "A_2", + "keyName": "COMPANY_EDIT", + "modifyDate": None, + "name": "Edit Company Profile" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Permission to manage account notes", + "id": 5087, + "key": None, + "keyName": "MANAGE_ACCOUNT_NOTE", + "modifyDate": None, + "name": "Manage Account Notes" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Manage e-mail delivery service accounts.", + "id": 1308, + "key": "NET_4", + "keyName": "NETWORK_MESSAGE_DELIVERY_MANAGE", + "modifyDate": None, + "name": "Manage E-mail Delivery Service" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Provides users ability to toggle the EU Supported account flag.", + "id": 3693, + "key": "EU_1", + "keyName": "EU_LIMITED_PROCESSING_MANAGE", + "modifyDate": None, + "name": "Manage EU Supported Account Flag" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Create and manage notification subscribers for usage warnings and overages.", + "id": 1296, + "key": "NTF_1", + "keyName": "NTF_SUBSCRIBER_MANAGE", + "modifyDate": None, + "name": "Manage Notification Subscribers" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Manage users and assign permissions.", + "id": 1266, + "key": "A_0", + "keyName": "USER_MANAGE", + "modifyDate": None, + "name": "Manage Users" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Physically Access a Customer's Colo Cage", + "id": 1319, + "key": "DA_2", + "keyName": "DATACENTER_ROOM_ACCESS", + "modifyDate": None, + "name": "Physically Access a Customer's Colo Cage" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Physically Access a Datacenter", + "id": 1318, + "key": "DA_1", + "keyName": "DATACENTER_ACCESS", + "modifyDate": None, + "name": "Physically Access a Datacenter" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Submit one-time payments for this account.", + "id": 63, + "key": "A_4", + "keyName": "ONE_TIME_PAYMENTS", + "modifyDate": None, + "name": "Submit One-Time Payments" + }, + { + "createDate": None, + "departmentId": 1, + "description": "Update the recurring monthly payment information.", + "id": 62, + "key": "A_3", + "keyName": "UPDATE_PAYMENT_DETAILS", + "modifyDate": None, + "name": "Update Payment Details" + }, + { + "createDate": None, + "departmentId": 1, + "description": "View the account summary page including invoices and payments.", + "id": 64, + "key": "A_1", + "keyName": "ACCOUNT_SUMMARY_VIEW", + "modifyDate": None, + "name": "View Account Summary" + }, + { + "createDate": None, + "departmentId": 1, + "description": "View the account-wide event log history.", + "id": 1314, + "key": "A_15", + "keyName": "USER_EVENT_LOG_VIEW", + "modifyDate": None, + "name": "View Event Log" + } + ] + }, + { + "description": "Sales", + "id": 2, + "keyName": "SALES", + "name": "Sales", + "permissions": [ + { + "createDate": None, + "departmentId": 2, + "description": "Add new servers to the account.", + "id": 1267, + "key": "XX_1", + "keyName": "SERVER_ADD", + "modifyDate": None, + "name": "Add Server" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Add and upgrade any cloud computing instances on the account.", + "id": 1303, + "key": "A_11", + "keyName": "INSTANCE_UPGRADE", + "modifyDate": None, + "name": "Add/Upgrade Cloud Instances" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Add and upgrade any services on the account.", + "id": 1271, + "key": "A_7", + "keyName": "SERVICE_ADD", + "modifyDate": None, + "name": "Add/Upgrade Services" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Add and upgrade any storage services (StorageLayer) on the account.", + "id": 1265, + "key": "A_10", + "keyName": "ADD_SERVICE_STORAGE", + "modifyDate": None, + "name": "Add/Upgrade Storage (StorageLayer)" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Cancel any servers on the account.", + "id": 1270, + "key": "A_6", + "keyName": "SERVER_CANCEL", + "modifyDate": None, + "name": "Cancel Server" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Cancel any services on the account.", + "id": 1273, + "key": "A_9", + "keyName": "SERVICE_CANCEL", + "modifyDate": None, + "name": "Cancel Services" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Upgrade any servers on the account.", + "id": 1269, + "key": "A_5", + "keyName": "SERVER_UPGRADE", + "modifyDate": None, + "name": "Upgrade Server" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Upgrade Services", + "id": 1272, + "key": "A_8", + "keyName": "SERVICE_UPGRADE", + "modifyDate": None, + "name": "Upgrade Services" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Permission to view billing ACH information.", + "id": 5085, + "key": None, + "keyName": "VIEW_ACH_INFO", + "modifyDate": None, + "name": "View Billing ACH Information" + }, + { + "createDate": None, + "departmentId": 2, + "description": "Permission to view an order with reseller pricing.", + "id": 5086, + "key": None, + "keyName": "VIEW_RESELLER_ORDER", + "modifyDate": None, + "name": "View reseller order pricing" + } + ] + }, + { + "description": "Support", + "id": 3, + "keyName": "SUPPORT", + "name": "Support", + "permissions": [ + { + "createDate": None, + "departmentId": 3, + "description": "Add new support tickets.", + "id": 3, + "key": "T_7", + "keyName": "TICKET_ADD", + "modifyDate": None, + "name": "Add Tickets" + }, + { + "createDate": None, + "departmentId": 3, + "description": "Edit support tickets.", + "id": 2, + "key": "T_8", + "keyName": "TICKET_EDIT", + "modifyDate": None, + "name": "Edit Tickets" + }, + { + "createDate": None, + "departmentId": 3, + "description": "Search through previous tickets.", + "id": 203, + "key": "T_2", + "keyName": "TICKET_SEARCH", + "modifyDate": None, + "name": "Search Tickets" + }, + { + "createDate": None, + "departmentId": 3, + "description": "View all tickets regardless of which user the ticket is assigned to.", + "id": 1321, + "key": "T_6", + "keyName": "TICKET_VIEW_ALL", + "modifyDate": None, + "name": "View All Tickets" + }, + { + "createDate": None, + "departmentId": 3, + "description": "View tickets assigned to the user.", + "id": 1, + "key": "T_1", + "keyName": "TICKET_VIEW", + "modifyDate": None, + "name": "View Tickets" + } + ] + }, + { + "description": "Security", + "id": 4, + "keyName": "SECURITY", + "name": "Security", + "permissions": [ + { + "createDate": None, + "departmentId": 4, + "description": "Add, remove, and update certificates (includes the private key).", + "id": 1325, + "key": "SE_9", + "keyName": "SECURITY_CERTIFICATE_MANAGE", + "modifyDate": None, + "name": "Manage Certificates (SSL)" + }, + { + "createDate": None, + "departmentId": 4, + "description": "Create, edit and delete SAML authentication records.", + "id": 3664, + "key": "SM_1", + "keyName": "SAML_AUTHENTICATION_MANAGE", + "modifyDate": None, + "name": "Manage SAML Authentication" + }, + { + "createDate": None, + "departmentId": 4, + "description": "Add, remove, and update SSH keys.", + "id": 1320, + "key": "SE_10", + "keyName": "CUSTOMER_SSH_KEY_MANAGEMENT", + "modifyDate": None, + "name": "Manage SSH Keys" + }, + { + "createDate": None, + "departmentId": 4, + "description": "Request compliance reports.", + "id": 2442, + "key": "COM_1", + "keyName": "REQUEST_COMPLIANCE_REPORT", + "modifyDate": None, + "name": "Request Compliance Report" + }, + { + "createDate": None, + "departmentId": 4, + "description": "View certificates (includes the private key).", + "id": 1324, + "key": "SE_8", + "keyName": "SECURITY_CERTIFICATE_VIEW", + "modifyDate": None, + "name": "View Certificates (SSL)" + }, + { + "createDate": None, + "departmentId": 4, + "description": "Request and view Vulnerability Scans.", + "id": 1295, + "key": "SE_7", + "keyName": "VULN_SCAN_MANAGE", + "modifyDate": None, + "name": "Vulnerability Scanning" + } + ] + }, + { + "description": "Devices", + "id": 5, + "keyName": "DEVICES", + "name": "Devices", + "permissions": [ + { + "createDate": None, + "departmentId": 5, + "description": "Allows a user to access virtual dedicated hosts", + "id": 3679, + "key": "ALL_3", + "keyName": "ACCESS_ALL_DEDICATEDHOSTS", + "modifyDate": None, + "name": "Access Virtual DedicatedHosts" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Add IP Addresses to a server.", + "id": 1281, + "key": "H_6", + "keyName": "IP_ADD", + "modifyDate": None, + "name": "Add IP Addresses" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allow a user to access all guests on the account.", + "id": 1841, + "key": "ALL_2", + "keyName": "ACCESS_ALL_GUEST", + "modifyDate": None, + "name": "All Guest Access" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allow a user to access all hardware on the account.", + "id": 1821, + "key": "ALL_1", + "keyName": "ACCESS_ALL_HARDWARE", + "modifyDate": None, + "name": "All Hardware Access" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Edit hostname and domain name for devices on the account.", + "id": 1304, + "key": "H_7", + "keyName": "HOSTNAME_EDIT", + "modifyDate": None, + "name": "Edit Hostname/Domain" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allows users to edit Hardware Component Hard Drive Dirty Attribute.", + "id": 3742, + "key": None, + "keyName": "HARDWARE_COMPONENT_DRIVE_DIRTY_ATTRIBUTE_EDIT", + "modifyDate": None, + "name": "Hardware Component Hard Drive Dirty Attribute Edit" + }, + { + "createDate": None, + "departmentId": 5, + "description": "View Host IDS logs.", + "id": 1294, + "key": "SE_6", + "keyName": "HOST_ID_MANAGE", + "modifyDate": None, + "name": "Host IDS" + }, + { + "createDate": None, + "departmentId": 5, + "description": "View IPMI details regarding hardware and issue reboot commands through the portal.", + "id": 1277, + "key": "H_2", + "keyName": "REMOTE_MANAGEMENT", + "modifyDate": None, + "name": "IPMI Remote Management" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allows users to manage configuration template.", + "id": 3748, + "key": None, + "keyName": "MANAGE_CONFIGURATION_TEMPLATE", + "modifyDate": None, + "name": "Manage Configuration Template" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allows users to manage customer hardware.", + "id": 3746, + "key": None, + "keyName": "MANAGE_CUSTOMER_HARDWARE", + "modifyDate": None, + "name": "Manage Customer Hardware" + }, + { + "createDate": None, + "departmentId": 5, + "description": "View and edit monitoring information for devices.", + "id": 1278, + "key": "H_3", + "keyName": "MONITORING_MANAGE", + "modifyDate": None, + "name": "Manage Device Monitoring" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Manage Customer Post Provisioning Scripts.", + "id": 541, + "key": "SO_8", + "keyName": "CUSTOMER_POST_PROVISION_SCRIPT_MANAGEMENT", + "modifyDate": None, + "name": "Manage Provisioning Scripts" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Manage Public Image Templates.", + "id": 1323, + "key": "I_1", + "keyName": "PUBLIC_IMAGE_MANAGE", + "modifyDate": None, + "name": "Manage Public Images" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Issue OS reloads and Rescue Kernel for devices.", + "id": 1279, + "key": "H_4", + "keyName": "SERVER_RELOAD", + "modifyDate": None, + "name": "OS Reloads and Rescue Kernel" + }, + { + "createDate": None, + "departmentId": 5, + "description": "View storage details and edit storage passwords.", + "id": 1283, + "key": "NAS_2", + "keyName": "NAS_MANAGE", + "modifyDate": None, + "name": "Storage Manage" + }, + { + "createDate": None, + "departmentId": 5, + "description": "View hardware information such as IP addresses, OS type, p", + "id": 163, + "key": "H_1", + "keyName": "HARDWARE_VIEW", + "modifyDate": None, + "name": "View Hardware Details" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allows users to view location reservation.", + "id": 3750, + "key": None, + "keyName": "VIEW_LOCATION_RESERVATION", + "modifyDate": None, + "name": "View Location Reservation" + }, + { + "createDate": None, + "departmentId": 5, + "description": "View virtual dedicated host information. ", + "id": 3684, + "key": "VH_1", + "keyName": "DEDICATED_HOST_VIEW", + "modifyDate": None, + "name": "View Virtual Dedicated Host Details" + }, + { + "createDate": None, + "departmentId": 5, + "description": "View virtual server information such as IP addresses, OS type, passwords, e", + "id": 1302, + "key": "VG_1", + "keyName": "VIRTUAL_GUEST_VIEW", + "modifyDate": None, + "name": "View Virtual Server Details" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allows users to view and edit dedicated host.", + "id": 6278, + "key": None, + "keyName": "MANAGE_DEDICATED_HOST", + "modifyDate": None, + "name": "View and edit dedicated host" + }, + { + "createDate": None, + "departmentId": 5, + "description": "Allows users to view and edit virtual guest data.", + "id": 6280, + "key": None, + "keyName": "MANAGE_VIRTUAL_GUEST", + "modifyDate": None, + "name": "View and edit virtual guest" + } + ] + }, + { + "description": "Network", + "id": 6, + "keyName": "NETWORK", + "name": "Network", + "permissions": [ + { + "createDate": None, + "departmentId": 6, + "description": "When adding compute (Server or Cloud Instance), a", + "id": 3682, + "key": "NET_6", + "keyName": "PUBLIC_NETWORK_COMPUTE", + "modifyDate": None, + "name": "Add Compute with Public Network Port" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage content delivery network account.", + "id": 1298, + "key": "CDN_1", + "keyName": "CDN_ACCOUNT_MANAGE", + "modifyDate": None, + "name": "Manage CDN Account" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage content delivery network file transfers.", + "id": 1299, + "key": "CDN_2", + "keyName": "CDN_FILE_MANAGE", + "modifyDate": None, + "name": "Manage CDN File Transfers" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Add, edit, and view DNS records managed by SoftLayer.", + "id": 1275, + "key": "DNS_1", + "keyName": "DNS_MANAGE", + "modifyDate": None, + "name": "Manage DNS" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage all firewall rules.", + "id": 1322, + "key": "FW_1", + "keyName": "FIREWALL_RULE_MANAGE", + "modifyDate": None, + "name": "Manage Firewall Rules" + }, + { + "createDate": None, + "departmentId": 6, + "description": "View and edit firewall logs and settings.", + "id": 1290, + "key": "SE_2", + "keyName": "FIREWALL_MANAGE", + "modifyDate": None, + "name": "Manage Firewalls" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage IPSEC network tunnels.", + "id": 250, + "key": "NET_3", + "keyName": "NETWORK_TUNNEL_MANAGE", + "modifyDate": None, + "name": "Manage IPSEC Network Tunnels" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage load balancers.", + "id": 1289, + "key": "LBS_1", + "keyName": "LOADBALANCER_MANAGE", + "modifyDate": None, + "name": "Manage Load Balancers" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage network gateway appliances.", + "id": 1842, + "key": "GTW_1", + "keyName": "GATEWAY_MANAGE", + "modifyDate": None, + "name": "Manage Network Gateways" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage network IDs", + "id": 1293, + "key": "SE_5", + "keyName": "NETWORK_IDS_MANAGE", + "modifyDate": None, + "name": "Manage Network IDs" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage network subnet routes.", + "id": 1301, + "key": "NET_1", + "keyName": "NETWORK_ROUTE_MANAGE", + "modifyDate": None, + "name": "Manage Network Subnet Routes" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Enable and disable private network VLAN spanning.", + "id": 1297, + "key": "NET_2", + "keyName": "NETWORK_VLAN_SPANNING", + "modifyDate": None, + "name": "Manage Network VLAN Spanning" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage port status and speeds for connected devices.", + "id": 1285, + "key": "PO_1", + "keyName": "PORT_CONTROL", + "modifyDate": None, + "name": "Manage Port Control" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Permission to connect and disconnect account with the private endpoint service.", + "id": 5048, + "key": None, + "keyName": "MANAGE_PRIVATE_ENDPOINT_SERVICE", + "modifyDate": None, + "name": "Manage Private Endpoint Service" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Permission to Manage the Public Network", + "id": 3672, + "key": "MP_1", + "keyName": "MANAGE_PUBLIC_NETWORK", + "modifyDate": None, + "name": "Manage Public Network" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage security groups.", + "id": 3678, + "key": "NET_5", + "keyName": "MANAGE_SECURITY_GROUPS", + "modifyDate": None, + "name": "Manage Security Groups" + }, + { + "createDate": None, + "departmentId": 6, + "description": "Manage VPN access for all users.", + "id": 1316, + "key": "VPN_1", + "keyName": "VPN_MANAGE", + "modifyDate": None, + "name": "VPN Administration" + }, + { + "createDate": None, + "departmentId": 6, + "description": "View bandwidth statistics and graphs for hardware.", + "id": 1274, + "key": "B_1", + "keyName": "BANDWIDTH_MANAGE", + "modifyDate": None, + "name": "View Bandwidth Statistics" + }, + { + "createDate": None, + "departmentId": 6, + "description": "View content delivery network bandwidth statistics.", + "id": 1300, + "key": "CDN_3", + "keyName": "CDN_BANDWIDTH_VIEW", + "modifyDate": None, + "name": "View CDN Bandwidth Statistics" + } + ] + }, + { + "description": "Software", + "id": 7, + "keyName": "SOFTWARE", + "name": "Software", + "permissions": [ + { + "createDate": None, + "departmentId": 7, + "description": "View and edit antivirus / spyware logs and settings.", + "id": 1292, + "key": "SE_4", + "keyName": "ANTI_MALWARE_MANAGE", + "modifyDate": None, + "name": "Manage Antivirus/Spyware" + }, + { + "createDate": None, + "departmentId": 7, + "description": "Manage firewall software", + "id": 1291, + "key": "SE_3", + "keyName": "SOFTWARE_FIREWALL_MANAGE", + "modifyDate": None, + "name": "Manage Firewall Software" + }, + { + "createDate": None, + "departmentId": 7, + "description": "Permission to initiate and delete an openstack link.", + "id": 3514, + "key": None, + "keyName": "OPENSTACK_LINK", + "modifyDate": None, + "name": "Openstack Link" + }, + { + "createDate": None, + "departmentId": 7, + "description": "View Customer Software Password", + "id": 240, + "key": "SO_9", + "keyName": "VIEW_CUSTOMER_SOFTWARE_PASSWORD", + "modifyDate": None, + "name": "View Customer Software Password" + }, + { + "createDate": None, + "departmentId": 7, + "description": "View login information for Helm.", + "id": 1263, + "key": "SO_3", + "keyName": "VIEW_HELM", + "modifyDate": None, + "name": "View Helm" + }, + { + "createDate": None, + "departmentId": 7, + "description": "View login information for Plesk.", + "id": 1262, + "key": "SO_2", + "keyName": "VIEW_PLESK", + "modifyDate": None, + "name": "View Plesk" + }, + { + "createDate": None, + "departmentId": 7, + "description": "View login information for QuantaStor.", + "id": 1317, + "key": "SO_7", + "keyName": "VIEW_QUANTASTOR", + "modifyDate": None, + "name": "View QuantaStor" + }, + { + "createDate": None, + "departmentId": 7, + "description": "View login information for Urchin.", + "id": 1264, + "key": "SO_4", + "keyName": "VIEW_URCHIN", + "modifyDate": None, + "name": "View Urchin" + }, + { + "createDate": None, + "departmentId": 7, + "description": "Allows users to view and edit disk image data.", + "id": 6279, + "key": None, + "keyName": "MANAGE_DISK_IMAGE", + "modifyDate": None, + "name": "View and edit disk image" + }, + { + "createDate": None, + "departmentId": 7, + "description": "Allows users to view and edit image template.", + "id": 6277, + "key": None, + "keyName": "IMAGE_TEMPLATE_MANAGE", + "modifyDate": None, + "name": "View and edit manage image template" + }, + { + "createDate": None, + "departmentId": 7, + "description": "Allows users to view and edit software component.", + "id": 6276, + "key": None, + "keyName": "SOFTWARE_MANAGE", + "modifyDate": None, + "name": "View and edit software component" + }, + { + "createDate": None, + "departmentId": 7, + "description": "View login information for cPanel.", + "id": 1261, + "key": "SO_1", + "keyName": "VIEW_CPANEL", + "modifyDate": None, + "name": "View cPanel" + }, + { + "createDate": None, + "departmentId": 7, + "description": "View licenses", + "id": 1280, + "key": "H_5", + "keyName": "LICENSE_VIEW", + "modifyDate": None, + "name": "View licenses" + }, + { + "createDate": None, + "departmentId": 7, + "description": "Allows users to view software account license.", + "id": 6275, + "key": None, + "keyName": "SOFTWARE_LICENSE_MANAGE", + "modifyDate": None, + "name": "View software account license" + } + ] + } +] diff --git a/SoftLayer/managers/user.py b/SoftLayer/managers/user.py index 8e854426c..72b23dfda 100644 --- a/SoftLayer/managers/user.py +++ b/SoftLayer/managers/user.py @@ -15,6 +15,7 @@ LOGGER = logging.getLogger(__name__) +# pylint: disable=too-many-public-methods class UserManager(utils.IdentifierMixin, object): """Manages Users. @@ -76,7 +77,7 @@ def get_current_user(self, objectmask=None): return self.account_service.getCurrentUser(mask=objectmask) def get_all_permissions(self): - """Calls SoftLayer_User_CustomerPermissions_Permission::getAllObjects + """Calls User_Permission_Action::getAllObjects Stores the result in self.all_permissions :returns: A list of dictionaries that contains all valid permissions @@ -86,6 +87,19 @@ def get_all_permissions(self): self.all_permissions = sorted(permissions, key=itemgetter('keyName')) return self.all_permissions + def get_permission_departments(self): + """Calls SoftLayer_User_Permission_Department::getAllObjects + + Stores the result in self.all_permissions + :returns: A list of dictionaries that contains all valid permissions + """ + mask = "mask[permissions[id,keyName,description,name]]" + departments = self.client.call('User_Permission_Department', 'getAllObjects', mask=mask) + for i, department in enumerate(departments): + departments[i]['permissions'] = sorted(department.get('permissions'), key=itemgetter('keyName')) + + return departments + def get_all_notifications(self): """Calls SoftLayer_Email_Subscription::getAllObjects diff --git a/tests/CLI/modules/user_tests.py b/tests/CLI/modules/user_tests.py index 804c9ef1d..514d662ac 100644 --- a/tests/CLI/modules/user_tests.py +++ b/tests/CLI/modules/user_tests.py @@ -113,7 +113,7 @@ def test_print_hardware_access(self): def test_permissions_list(self): result = self.run_command(['user', 'permissions', '11100']) self.assert_no_fail(result) - self.assert_called_with('SoftLayer_User_Permission_Action', 'getAllObjects') + self.assert_called_with('SoftLayer_User_Permission_Department', 'getAllObjects') self.assert_called_with( 'SoftLayer_User_Customer', 'getObject', identifier=11100, mask='mask[id, permissions, isMasterUserFlag, roles]' diff --git a/tests/managers/user_tests.py b/tests/managers/user_tests.py index b5b5f9da1..f129e0c10 100644 --- a/tests/managers/user_tests.py +++ b/tests/managers/user_tests.py @@ -364,3 +364,10 @@ def test_get_api_authentication_keys(self): def test_remove_api_authentication_key(self): self.manager.remove_api_authentication_key(123456) self.assert_called_with('SoftLayer_User_Customer', 'removeApiAuthenticationKey') + + def test_get_permission_departments(self): + result = self.manager.get_permission_departments() + self.assert_called_with('SoftLayer_User_Permission_Department', 'getAllObjects') + # just making sure the lists are sorted. + self.assertEqual(result[0]['permissions'][0]['keyName'], 'ACCOUNT_BILLING_SYSTEM') + self.assertEqual(result[1]['permissions'][8]['keyName'], 'VIEW_ACH_INFO') From 576ab309a287ea7a3ff7e51163d884e8435aa8cb Mon Sep 17 00:00:00 2001 From: d3rn <84808889+d3rnn@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:55:11 -0500 Subject: [PATCH 08/77] Update list.py --- SoftLayer/CLI/virt/list.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index b16d5602f..b350f75bc 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -21,7 +21,8 @@ column_helper.Column('action', lambda guest: formatting.active_txn(guest), mask='activeTransaction[id,transactionStatus[name,friendlyName]]'), column_helper.Column('power_state', ('powerState', 'name')), - column_helper.Column('created_by', ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), + column_helper.Column('created_by', lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column('tags', lambda server: formatting.tags(server.get('tagReferences')), mask="tagReferences.tag.name"), column_helper.Column( From c1789ff0fc84dc8c2f653ad86b75359fbefd0f3e Mon Sep 17 00:00:00 2001 From: d3rn <84808889+d3rnn@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:55:47 -0500 Subject: [PATCH 09/77] Update list.py --- SoftLayer/CLI/hardware/list.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/hardware/list.py b/SoftLayer/CLI/hardware/list.py index 42b298155..0dae57a11 100644 --- a/SoftLayer/CLI/hardware/list.py +++ b/SoftLayer/CLI/hardware/list.py @@ -8,6 +8,7 @@ from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +from SoftLayer import utils # pylint: disable=unnecessary-lambda @@ -22,7 +23,8 @@ mask='activeTransaction[id, transactionStatus[name, friendlyName]]'), column_helper.Column( 'created_by', - ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), + lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column( 'tags', lambda server: formatting.tags(server.get('tagReferences')), From 6581ed122b7e40cd0f2f13de41583e3700632833 Mon Sep 17 00:00:00 2001 From: d3rn <84808889+d3rnn@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:56:15 -0500 Subject: [PATCH 10/77] Update list_guests.py --- SoftLayer/CLI/dedicatedhost/list_guests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/dedicatedhost/list_guests.py b/SoftLayer/CLI/dedicatedhost/list_guests.py index 6d263941d..bf00a236c 100644 --- a/SoftLayer/CLI/dedicatedhost/list_guests.py +++ b/SoftLayer/CLI/dedicatedhost/list_guests.py @@ -8,6 +8,7 @@ from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +from SoftLayer import utils COLUMNS = [ column_helper.Column('guid', ('globalIdentifier',)), @@ -18,7 +19,8 @@ column_helper.Column('backend_ip', ('primaryBackendIpAddress',)), column_helper.Column( 'created_by', - ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), + lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column('power_state', ('powerState', 'name')), column_helper.Column( 'tags', From 5c411a61e4b0d6ffda39651a86003e131a55f6db Mon Sep 17 00:00:00 2001 From: d3rnn <84808889+d3rnn@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:24:30 +0000 Subject: [PATCH 11/77] fix E131 and E501 --- SoftLayer/CLI/dedicatedhost/list_guests.py | 4 ++-- SoftLayer/CLI/hardware/list.py | 4 ++-- SoftLayer/CLI/virt/list.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/list_guests.py b/SoftLayer/CLI/dedicatedhost/list_guests.py index bf00a236c..5d8f2c3bc 100644 --- a/SoftLayer/CLI/dedicatedhost/list_guests.py +++ b/SoftLayer/CLI/dedicatedhost/list_guests.py @@ -19,8 +19,8 @@ column_helper.Column('backend_ip', ('primaryBackendIpAddress',)), column_helper.Column( 'created_by', - lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), - mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), + lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column('power_state', ('powerState', 'name')), column_helper.Column( 'tags', diff --git a/SoftLayer/CLI/hardware/list.py b/SoftLayer/CLI/hardware/list.py index 0dae57a11..734f379d4 100644 --- a/SoftLayer/CLI/hardware/list.py +++ b/SoftLayer/CLI/hardware/list.py @@ -23,8 +23,8 @@ mask='activeTransaction[id, transactionStatus[name, friendlyName]]'), column_helper.Column( 'created_by', - lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), - mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), + lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column( 'tags', lambda server: formatting.tags(server.get('tagReferences')), diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index b350f75bc..b491c471e 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -21,7 +21,8 @@ column_helper.Column('action', lambda guest: formatting.active_txn(guest), mask='activeTransaction[id,transactionStatus[name,friendlyName]]'), column_helper.Column('power_state', ('powerState', 'name')), - column_helper.Column('created_by', lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + column_helper.Column('created_by', + lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column('tags', lambda server: formatting.tags(server.get('tagReferences')), mask="tagReferences.tag.name"), From 3adf56cdee1194f22736db3057d7a99e17d8d900 Mon Sep 17 00:00:00 2001 From: d3rnn <84808889+d3rnn@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:30:23 +0000 Subject: [PATCH 12/77] fix W291 and E501 --- SoftLayer/CLI/virt/list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index b491c471e..d30b4ff6a 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -21,8 +21,8 @@ column_helper.Column('action', lambda guest: formatting.active_txn(guest), mask='activeTransaction[id,transactionStatus[name,friendlyName]]'), column_helper.Column('power_state', ('powerState', 'name')), - column_helper.Column('created_by', - lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), + column_helper.Column('created_by', lambda created_by: + utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column('tags', lambda server: formatting.tags(server.get('tagReferences')), mask="tagReferences.tag.name"), From 071683b73e970d1e3ba75220219bee933b91ebf7 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 12 Jul 2024 13:39:14 -0500 Subject: [PATCH 13/77] Updated CodeQL Jobs, Fixed #2036 --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index adc5408b6..dc02956df 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 53d3871bb0a3a644efbf7ee02a05d2b0f106faba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:27:20 +0000 Subject: [PATCH 14/77] pip prod(deps): bump sphinx from 7.3.7 to 7.4.4 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.3.7 to 7.4.4. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.3.7...v7.4.4) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index a421e1702..36adedad3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx==7.3.7 +sphinx==7.4.4 sphinx_rtd_theme==2.0.0 sphinx-click==6.0.0 click From 2899f68f0740bc10a95a3139f87b0999a2fd9f78 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 17 Jul 2024 16:12:18 -0500 Subject: [PATCH 15/77] v6.2.3 version bump --- SoftLayer/consts.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 2bc9bd20a..02cc3352e 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v6.2.2' +VERSION = 'v6.2.3' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/setup.py b/setup.py index d4490fe20..fd008d57f 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='v6.2.2', + version='v6.2.3', description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type='text/x-rst', From e6f5281245f47eaab25bb6be17ce4f952fa9325a Mon Sep 17 00:00:00 2001 From: Christine K <125943705+karistom@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:23:01 +1000 Subject: [PATCH 16/77] update --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cf2fc9885..39b0dfd4a 100644 --- a/README.rst +++ b/README.rst @@ -174,7 +174,7 @@ Python Packages --------------- * prettytable >= 2.5.0 * click >= 8.0.4 -* requests >= 2.20.0 +* requests >= 2.32.2 * prompt_toolkit >= 2 * pygments >= 2.0.0 * urllib3 >= 1.24 From 60dcf16bcb589aad440c0172dc2d2709da3a00b2 Mon Sep 17 00:00:00 2001 From: Christine K <125943705+karistom@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:23:53 +1000 Subject: [PATCH 17/77] update requests --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fd008d57f..5ab47f220 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires=[ 'prettytable >= 2.5.0', 'click >= 8.0.4', - 'requests >= 2.20.0', + 'requests >= 2.32.2', 'prompt_toolkit >= 2', 'pygments >= 2.0.0', 'urllib3 >= 1.24', From f297f2892259a6c3a25142ace43edde5147eff4c Mon Sep 17 00:00:00 2001 From: Christine K <125943705+karistom@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:24:19 +1000 Subject: [PATCH 18/77] update requests --- tools/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements.txt b/tools/requirements.txt index 66abd689c..68582aec9 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,6 +1,6 @@ prettytable >= 2.5.0 click >= 8.0.4 -requests >= 2.20.0 +requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 From c23572515af2db88ed69ea1e60326cf5a94368a5 Mon Sep 17 00:00:00 2001 From: Christine K <125943705+karistom@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:24:40 +1000 Subject: [PATCH 19/77] update requests --- tools/test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/test-requirements.txt b/tools/test-requirements.txt index 6b546ccf3..e40183675 100644 --- a/tools/test-requirements.txt +++ b/tools/test-requirements.txt @@ -6,7 +6,7 @@ mock sphinx prettytable >= 2.5.0 click >= 8.0.4 -requests >= 2.20.0 +requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 From 487daa78ac5739a9d2cd8a172fbaf3af2df66b12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 03:38:19 +0000 Subject: [PATCH 20/77] pip prod(deps): bump sphinx from 7.4.4 to 7.4.7 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.4.4 to 7.4.7. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.4.4...v7.4.7) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 36adedad3..02463a499 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx==7.4.4 +sphinx==7.4.7 sphinx_rtd_theme==2.0.0 sphinx-click==6.0.0 click From 6783d32b6f3c2532129fb80cae30e45ce6d9c116 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 25 Jul 2024 11:14:58 -0500 Subject: [PATCH 21/77] Update codeql-analysis.yml to v3 Missed the autobuild version, updating that --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dc02956df..d43890b44 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl From cf92990c0f68d044d899b853a92fa5c44d281024 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:29:53 +0000 Subject: [PATCH 22/77] pip prod(deps): bump sphinx from 7.4.7 to 8.0.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.4.7 to 8.0.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.4.7...v8.0.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 02463a499..0dffff663 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx==7.4.7 +sphinx==8.0.0 sphinx_rtd_theme==2.0.0 sphinx-click==6.0.0 click From a1844999db12a9558734f075094d2b10b67e1733 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 29 Jul 2024 17:14:33 -0500 Subject: [PATCH 23/77] V6.2.4 version bump --- SoftLayer/consts.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 02cc3352e..0b20929f6 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v6.2.3' +VERSION = 'v6.2.4' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/setup.py b/setup.py index 5ab47f220..b930bb8ad 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='v6.2.3', + version='v6.2.4', description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type='text/x-rst', From 0a8ca6be8c92fbad4b76005aecfef750ada4a3a2 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 31 Jul 2024 15:49:45 -0500 Subject: [PATCH 24/77] Added x509 certificate support to cli --- SoftLayer/API.py | 6 ++++-- SoftLayer/config.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 0a2788b63..00dbfe74f 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -211,9 +211,11 @@ def employee_client(username=None, access_token = settings.get('access_token') user_id = settings.get('userid') - # Assume access_token is valid for now, user has logged in before at least. - if access_token and user_id: + if settings.get('auth_cert', False): + auth = slauth.X509Authentication(settings.get('auth_cert'), verify) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + elif access_token and user_id: auth = slauth.EmployeeAuthentication(user_id, access_token) return EmployeeClient(auth=auth, transport=transport, config_file=config_file) else: diff --git a/SoftLayer/config.py b/SoftLayer/config.py index e695d6b9a..f67552639 100644 --- a/SoftLayer/config.py +++ b/SoftLayer/config.py @@ -61,7 +61,8 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'proxy': '', 'userid': '', 'access_token': '', - 'verify': "True" + 'verify': "True", + 'auth_cert': '' }) config.read(config_files) @@ -74,7 +75,8 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'api_key': config.get('softlayer', 'api_key'), 'userid': config.get('softlayer', 'userid'), 'access_token': config.get('softlayer', 'access_token'), - 'verify': config.get('softlayer', 'verify') + 'verify': config.get('softlayer', 'verify'), + 'auth_cert': config.get('softlayer', 'auth_cert') } if r_config["verify"].lower() == "true": r_config["verify"] = True From 0bed0d863c8ee2a9b1fa52931d5fcee3b7d457d3 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 31 Jul 2024 16:04:03 -0500 Subject: [PATCH 25/77] v6.2.5 updates --- SoftLayer/consts.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 0b20929f6..136e2800d 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v6.2.4' +VERSION = 'v6.2.5' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/setup.py b/setup.py index b930bb8ad..045cb6d75 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='v6.2.4', + version='v6.2.5', description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type='text/x-rst', From edf3fff46765c0a4d0bee9317f158a3f3fba9814 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:01:31 +0000 Subject: [PATCH 26/77] pip prod(deps): bump sphinx from 8.0.0 to 8.0.2 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.0.0 to 8.0.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.0.2/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.0.0...v8.0.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 0dffff663..2c1e4075b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx==8.0.0 +sphinx==8.0.2 sphinx_rtd_theme==2.0.0 sphinx-click==6.0.0 click From ec89f09de58fd5755103c51d0a8c740b8873246d Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 5 Aug 2024 17:51:13 -0500 Subject: [PATCH 27/77] Reverted 'globalip assign' syntax while still using the Network_Subnet::route() method. Fixed #2176 --- SoftLayer/CLI/dns/zone_delete.py | 1 + SoftLayer/CLI/formatting.py | 3 +- SoftLayer/CLI/globalip/assign.py | 46 ++++++++++++++----- SoftLayer/CLI/globalip/cancel.py | 4 +- SoftLayer/CLI/globalip/unassign.py | 20 ++++++-- ...ftLayer_Network_Subnet_IpAddress_Global.py | 2 +- SoftLayer/managers/network.py | 6 +-- tests/CLI/modules/globalip_tests.py | 37 +++++++++++---- 8 files changed, 87 insertions(+), 32 deletions(-) diff --git a/SoftLayer/CLI/dns/zone_delete.py b/SoftLayer/CLI/dns/zone_delete.py index 83eb11273..cca4c9c9e 100644 --- a/SoftLayer/CLI/dns/zone_delete.py +++ b/SoftLayer/CLI/dns/zone_delete.py @@ -17,6 +17,7 @@ def cli(env, zone): """Delete zone. Example:: + slcli dns zone-delete ibm.com This command deletes a zone that is named ibm.com """ diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index c4c284636..0e51eb308 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -254,8 +254,7 @@ def confirm(prompt_str, default=False): def no_going_back(confirmation): """Show a confirmation to a user. - :param confirmation str: the string the user has to enter in order to - confirm their action. + :param confirmation str: the string the user has to enter in order to confirm their action. """ if not confirmation: confirmation = 'yes' diff --git a/SoftLayer/CLI/globalip/assign.py b/SoftLayer/CLI/globalip/assign.py index 1e793761e..a03d83744 100644 --- a/SoftLayer/CLI/globalip/assign.py +++ b/SoftLayer/CLI/globalip/assign.py @@ -5,27 +5,51 @@ import SoftLayer from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers -target_types = {'vlan': 'SoftLayer_Network_Vlan', - 'ip': 'SoftLayer_Network_Subnet_IpAddress', - 'hardware': 'SoftLayer_Hardware_Server', - 'vsi': 'SoftLayer_Virtual_Guest'} + +# pylint: disable=unused-argument +def targetipcallback(ctx, param, value): + """This is here to allow for using --target-id in some cases. Takes the first value and returns it""" + if value: + return value[0] + return value @click.command(cls=SoftLayer.CLI.command.SLCommand, epilog="More information about types and identifiers " "on https://sldn.softlayer.com/reference/services/SoftLayer_Network_Subnet/route/") -@click.argument('identifier') +@click.argument('globalip') +@click.argument('targetip', nargs=-1, callback=targetipcallback) @click.option('--target', type=click.Choice(['vlan', 'ip', 'hardware', 'vsi']), help='choose the type. vlan, ip, hardware, vsi') -@click.option('--target-id', help='The identifier for the destination resource to route this subnet to. ') +@click.option('--target-id', help='The identifier for the destination resource to route this subnet to.') @environment.pass_env -def cli(env, identifier, target, target_id): - """Assigns the subnet to a target. +def cli(env, globalip, targetip, target, target_id): + """Assigns the GLOBALIP to TARGETIP. + GLOBALIP should be either the Global IP address, or the SoftLayer_Network_Subnet_IpAddress_Global id + See `slcli globalip list` + TARGETIP should be either the target IP address, or the SoftLayer_Network_Subnet_IpAddress id + See `slcli subnet list` Example:: + slcli globalip assign 12345678 9.111.123.456 - This command assigns IP address with ID 12345678 to a target device whose IP address is 9.111.123.456 - """ + This command assigns Global IP address with ID 12345678 to a target device whose IP address is 9.111.123.456 + slcli globalip assign 123.4.5.6 6.5.4.123 + Global IPs can be specified by their IP address + """ mgr = SoftLayer.NetworkManager(env.client) - mgr.route(identifier, target_types.get(target), target_id) + # Find SoftLayer_Network_Subnet_IpAddress_Global::id + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, globalip, name='Global IP') + + # Find Global IPs SoftLayer_Network_Subnet::id + mask = "mask[id,ipAddress[subnetId]]" + subnet = env.client.call('SoftLayer_Network_Subnet_IpAddress_Global', 'getObject', id=global_ip_id, mask=mask) + subnet_id = subnet.get('ipAddress', {}).get('subnetId') + + # For backwards compatibility + if target_id: + targetip = target_id + + mgr.route(subnet_id, 'SoftLayer_Network_Subnet_IpAddress', targetip) diff --git a/SoftLayer/CLI/globalip/cancel.py b/SoftLayer/CLI/globalip/cancel.py index 0d9394b24..920d07c71 100644 --- a/SoftLayer/CLI/globalip/cancel.py +++ b/SoftLayer/CLI/globalip/cancel.py @@ -18,12 +18,12 @@ def cli(env, identifier, force): """Cancel global IP. Example:: + slcli globalip cancel 12345 """ mgr = SoftLayer.NetworkManager(env.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, - name='global ip') + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, name='global ip') if not force: if not (env.skip_confirmations or diff --git a/SoftLayer/CLI/globalip/unassign.py b/SoftLayer/CLI/globalip/unassign.py index 563ebb106..564c74a8f 100644 --- a/SoftLayer/CLI/globalip/unassign.py +++ b/SoftLayer/CLI/globalip/unassign.py @@ -12,9 +12,21 @@ @click.argument('identifier') @environment.pass_env def cli(env, identifier): - """Unassigns a global IP from a target.""" + """Unroutes IDENTIFIER + + IDENTIFIER should be either the Global IP address, or the SoftLayer_Network_Subnet_IpAddress_Global id + Example:: + + slcli globalip unassign 123456 + + slcli globalip unassign 123.43.22.11 +""" mgr = SoftLayer.NetworkManager(env.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, - name='global ip') - mgr.unassign_global_ip(global_ip_id) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, name='global ip') + + # Find Global IPs SoftLayer_Network_Subnet::id + mask = "mask[id,ipAddress[subnetId]]" + subnet = env.client.call('SoftLayer_Network_Subnet_IpAddress_Global', 'getObject', id=global_ip_id, mask=mask) + subnet_id = subnet.get('ipAddress', {}).get('subnetId') + mgr.clear_route(subnet_id) diff --git a/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py b/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py index 89cd22f50..39244730b 100644 --- a/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py +++ b/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py @@ -1,3 +1,3 @@ route = True unroute = True -getObject = {'id': 1234, 'billingItem': {'id': 1234}} +getObject = {'id': 1234, 'billingItem': {'id': 1234}, 'ipAddress': {'subnetId': 9988}} diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 201864606..49af7197f 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -841,12 +841,12 @@ def get_closed_pods(self): def route(self, subnet_id, type_serv, target): """Assigns a subnet to a specified target. - :param int subnet_id: The ID of the global IP being assigned + https://sldn.softlayer.com/reference/services/SoftLayer_Network_Subnet/route/ + :param int subnet_id: The ID of the SoftLayer_Network_Subnet_IpAddress being routed :param string type_serv: The type service to assign :param string target: The instance to assign """ - return self.client.call('SoftLayer_Network_Subnet', 'route', - type_serv, target, id=subnet_id, ) + return self.client.call('SoftLayer_Network_Subnet', 'route', type_serv, target, id=subnet_id, ) def get_datacenter(self, _filter=None, datacenter=None): """Calls SoftLayer_Location::getDatacenters() diff --git a/tests/CLI/modules/globalip_tests.py b/tests/CLI/modules/globalip_tests.py index 43c5b0f4d..a309d5636 100644 --- a/tests/CLI/modules/globalip_tests.py +++ b/tests/CLI/modules/globalip_tests.py @@ -12,15 +12,9 @@ import json -class DnsTests(testing.TestCase): +class GlobalIpTests(testing.TestCase): - def test_ip_assign(self): - result = self.run_command(['globalip', 'assign', '1']) - - self.assert_no_fail(result) - self.assertEqual(result.output, "") - - @mock.patch('SoftLayer.CLI.formatting.no_going_back') + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_ip_cancel(self, no_going_back_mock): # Test using --really flag result = self.run_command(['--really', 'globalip', 'cancel', '1']) @@ -39,7 +33,7 @@ def test_ip_cancel(self, no_going_back_mock): no_going_back_mock.return_value = False result = self.run_command(['globalip', 'cancel', '1']) - self.assertEqual(result.exit_code, 0) + self.assertEqual(result.exit_code, 2) def test_ip_list(self): result = self.run_command(['globalip', 'list', '--ip-version=v4']) @@ -84,6 +78,31 @@ def test_ip_unassign(self): result = self.run_command(['globalip', 'unassign', '1']) self.assert_no_fail(result) self.assertEqual(result.output, "") + self.assert_called_with('SoftLayer_Network_Subnet', 'clearRoute', identifier=9988) + + def test_ip_assign(self): + result = self.run_command(['globalip', 'assign', '1', '999']) + self.assert_no_fail(result) + self.assertEqual(result.output, "") + service = 'SoftLayer_Network_Subnet_IpAddress' + self.assert_called_with('SoftLayer_Network_Subnet', 'route', identifier=9988, args=(service, '999')) + + def test_ip_assign_target(self): + result = self.run_command(['globalip', 'assign', '1', '--target-id=8123']) + self.assert_no_fail(result) + self.assertEqual(result.output, "") + service = 'SoftLayer_Network_Subnet_IpAddress' + self.assert_called_with('SoftLayer_Network_Subnet', 'route', identifier=9988, args=(service, '8123')) + + def test_ip_assign_ip(self): + mock_api = self.set_mock('SoftLayer_Account', 'getGlobalIpRecords') + mock_api.return_value = [{"id": 112233}] + result = self.run_command(['globalip', 'assign', '192.168.1.1', '1.2.3.4']) + self.assert_no_fail(result) + self.assertEqual(result.output, "") + service = 'SoftLayer_Network_Subnet_IpAddress' + self.assert_called_with(f"{service}_Global", "getObject", identifier=112233) + self.assert_called_with('SoftLayer_Network_Subnet', 'route', identifier=9988, args=(service, '1.2.3.4')) def test_ip_cancel_force(self): result = self.run_command(['globalip', 'cancel', '1', '--force']) From b535762127d481a2e8e5aa9dc56c361dc69cf042 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 6 Aug 2024 17:35:42 -0500 Subject: [PATCH 28/77] Added vpn status to user list. Fixed #2178 --- SoftLayer/CLI/user/list.py | 6 ++++-- SoftLayer/managers/user.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/user/list.py b/SoftLayer/CLI/user/list.py index 17f3c8f69..779f14011 100644 --- a/SoftLayer/CLI/user/list.py +++ b/SoftLayer/CLI/user/list.py @@ -20,7 +20,8 @@ column_helper.Column('hardwareCount', ('hardwareCount',)), column_helper.Column('virtualGuestCount', ('virtualGuestCount',)), column_helper.Column('2FA', (TWO_FACTO_AUTH,)), - column_helper.Column('classicAPIKey', (CLASSIC_API_KEYS,)) + column_helper.Column('classicAPIKey', (CLASSIC_API_KEYS,)), + column_helper.Column('vpn', ('sslVpnAllowedFlag',)) ] DEFAULT_COLUMNS = [ @@ -30,6 +31,7 @@ 'displayName', '2FA', 'classicAPIKey', + 'vpn' ] @@ -48,7 +50,7 @@ def cli(env, columns): table = formatting.Table(columns.columns) for user in users: - user = _yes_format(user, [TWO_FACTO_AUTH, CLASSIC_API_KEYS]) + user = _yes_format(user, [TWO_FACTO_AUTH, CLASSIC_API_KEYS, 'sslVpnAllowedFlag']) table.add_row([value or formatting.blank() for value in columns.row(user)]) diff --git a/SoftLayer/managers/user.py b/SoftLayer/managers/user.py index 72b23dfda..87a5f0791 100644 --- a/SoftLayer/managers/user.py +++ b/SoftLayer/managers/user.py @@ -54,7 +54,7 @@ def list_users(self, objectmask=None, objectfilter=None): if objectmask is None: objectmask = """mask[id, username, displayName, userStatus[name], hardwareCount, virtualGuestCount, - email, roles, externalBindingCount,apiAuthenticationKeyCount]""" + email, roles, externalBindingCount,apiAuthenticationKeyCount, sslVpnAllowedFlag]""" return self.account_service.getUsers(mask=objectmask, filter=objectfilter) From 337514079aa53f6ee1d30d0a5a8fa50b07ce5823 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 9 Aug 2024 15:39:53 -0500 Subject: [PATCH 29/77] Fixed a bug when displaying empty tables. Fixed #2165 --- SoftLayer/CLI/formatting.py | 7 +++++-- SoftLayer/CLI/virt/detail.py | 30 +++++++---------------------- tests/CLI/formatting_table_tests.py | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index 0e51eb308..b9eca571e 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -70,7 +70,6 @@ def format_output(data, fmt='table', theme=None): # pylint: disable=R0911,R0912 return output # fallback, convert this odd object to a string - # print(f"Casting this to string {data}") return str(data) @@ -318,12 +317,16 @@ def __init__(self, columns, title=None, align=None): self.sortby = None self.title = title # Used to print a message if the table is empty - self.empty_message = None + self.empty_message = "-" def __bool__(self): """Useful for seeing if the table has any rows""" return len(self.rows) > 0 + def __str__(self): + """A Table should only be cast to a string if its empty""" + return self.empty_message + def set_empty_message(self, message): """Sets the empty message for this table for env.fout diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 958c865b1..041375d86 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -17,8 +17,7 @@ @click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('identifier') -@click.option('--passwords', - is_flag=True, +@click.option('--passwords', is_flag=True, help='Show passwords (check over your shoulder!)') @click.option('--price', is_flag=True, help='Show associated prices') @environment.pass_env @@ -53,10 +52,7 @@ def cli(env, identifier, passwords=False, price=False): table.add_row(['active_transaction', formatting.active_txn(result)]) table.add_row(['datacenter', result['datacenter']['name'] or formatting.blank()]) _cli_helper_dedicated_host(env, result, table) - operating_system = utils.lookup(result, - 'operatingSystem', - 'softwareLicense', - 'softwareDescription') or {} + operating_system = utils.lookup(result, 'operatingSystem', 'softwareLicense', 'softwareDescription') or {} table.add_row(['os', operating_system.get('name', '-')]) table.add_row(['os_version', operating_system.get('version', '-')]) table.add_row(['cores', result['maxCpu']]) @@ -76,10 +72,7 @@ def cli(env, identifier, passwords=False, price=False): table.add_row(['last_transaction', last_transaction]) table.add_row(['billing', 'Hourly' if result['hourlyBillingFlag'] else 'Monthly']) - table.add_row(['preset', utils.lookup(result, 'billingItem', - 'orderItem', - 'preset', - 'keyName') or '-']) + table.add_row(['preset', utils.lookup(result, 'billingItem', 'orderItem', 'preset', 'keyName') or '-']) table.add_row(_get_owner_row(result)) table.add_row(_get_vlan_table(result)) @@ -94,9 +87,7 @@ def cli(env, identifier, passwords=False, price=False): table.add_row(['notes', result.get('notes', '-')]) if price: - total_price = utils.lookup(result, - 'billingItem', - 'nextInvoiceTotalRecurringAmount') or 0 + total_price = utils.lookup(result, 'billingItem', 'nextInvoiceTotalRecurringAmount') or 0 if total_price != 0: table.add_row(['Prices', _price_table(utils.lookup(result, 'billingItem'), total_price)]) table.add_row(['Price rate', total_price]) @@ -107,10 +98,7 @@ def cli(env, identifier, passwords=False, price=False): for component in result['softwareComponents']: for item in component['passwords']: pass_table.add_row([ - utils.lookup(component, - 'softwareLicense', - 'softwareDescription', - 'name'), + utils.lookup(component, 'softwareLicense', 'softwareDescription', 'name'), item['username'], item['password'], ]) @@ -122,10 +110,7 @@ def cli(env, identifier, passwords=False, price=False): # Test to see if this actually has a primary (public) ip address try: if not result['privateNetworkOnlyFlag']: - ptr_domains = env.client.call( - 'Virtual_Guest', 'getReverseDomainRecords', - id=vs_id, - ) + ptr_domains = env.client.call('Virtual_Guest', 'getReverseDomainRecords', id=vs_id) for ptr_domain in ptr_domains: for ptr in ptr_domain['resourceRecords']: @@ -196,8 +181,7 @@ def _get_vlan_table(result): vlan_table = formatting.Table(['type', 'number', 'id']) for vlan in result['networkVlans']: - vlan_table.add_row([ - vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) + vlan_table.add_row([vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) return ['vlans', vlan_table] diff --git a/tests/CLI/formatting_table_tests.py b/tests/CLI/formatting_table_tests.py index 4d62a742b..f59764231 100644 --- a/tests/CLI/formatting_table_tests.py +++ b/tests/CLI/formatting_table_tests.py @@ -48,6 +48,26 @@ def test_key_value_table(self): result = capture.get() self.assertEqual(expected, result) + def test_key_value_table_empty(self): + + expected = """┌────────┬───────┐ +│ name │ value │ +├────────┼───────┤ +│ table2 │ - │ +└────────┴───────┘ +""" + table1 = formatting.KeyValueTable(["name", "value"]) + table2 = formatting.Table(["one", "two", "three"]) + table1.add_row(["table2", table2]) + result = formatting.format_output(table1, "table") + console = Console() + + with console.capture() as capture: + to_print = formatting.format_output(table1) + console.print(to_print) + result = capture.get() + self.assertEqual(expected, result) + def test_unrenderable_recovery_table(self): expected = """│ Sub Table │ [ Date: Fri, 9 Aug 2024 15:58:14 -0500 Subject: [PATCH 30/77] Updated snapcraft build to hopefully fix build errors. Fixed #2167 --- snap/snapcraft.yaml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 42375ded7..4b78a2c9d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -3,6 +3,7 @@ adopt-info: slcli summary: A CLI tool to interact with the SoftLayer API. description: | A command-line interface is also included and can be used to manage various SoftLayer products and services. + SLCLI documentation can be found here: https://softlayer-python.readthedocs.io/en/latest/ license: MIT @@ -10,14 +11,9 @@ base: core22 grade: stable confinement: strict -assumes: - - command-chain - apps: slcli: command: bin/slcli - command-chain: - - bin/homeishome-launch environment: LC_ALL: C.UTF-8 plugs: @@ -38,9 +34,4 @@ parts: - python3 stage-packages: - - python3 - - homeishome-launch: - plugin: nil - stage-snaps: - - homeishome-launch + - python3 From c4bded12ddc622983c3f368093a56cac97981a88 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 9 Aug 2024 16:01:46 -0500 Subject: [PATCH 31/77] Improved the formatting of hardware details page. Fixed #2153 --- SoftLayer/CLI/hardware/detail.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py index fe8661153..404a3235f 100644 --- a/SoftLayer/CLI/hardware/detail.py +++ b/SoftLayer/CLI/hardware/detail.py @@ -173,10 +173,13 @@ def _bw_table(bw_data): def _system_table(system_data): - table = formatting.Table(['Type', 'name']) + table = formatting.Table(['Type', 'Name']) + table.align['Type'] = 'r' + table.align['Name'] = 'l' + table.sortby = 'Type' for system in system_data: - table.add_row([utils.lookup(system, 'hardwareComponentModel', - 'hardwareGenericComponentModel', - 'hardwareComponentType', 'keyName'), - utils.lookup(system, 'hardwareComponentModel', 'longDescription')]) + c_type = utils.lookup(system, 'hardwareComponentModel', 'hardwareGenericComponentModel', + 'hardwareComponentType', 'keyName') + c_name = utils.lookup(system, 'hardwareComponentModel', 'longDescription') + table.add_row([c_type, c_name]) return table From 91af243ce86722f738e7c1d6b09d44704c416aa9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:00:18 +0000 Subject: [PATCH 32/77] pip prod(deps): bump rich from 13.7.1 to 13.8.0 Bumps [rich](https://github.com/Textualize/rich) from 13.7.1 to 13.8.0. - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v13.7.1...v13.8.0) --- updated-dependencies: - dependency-name: rich dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- setup.py | 2 +- tools/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 045cb6d75..204759dff 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ 'prompt_toolkit >= 2', 'pygments >= 2.0.0', 'urllib3 >= 1.24', - 'rich == 13.7.1' + 'rich == 13.8.0' ], keywords=['softlayer', 'cloud', 'slcli', 'ibmcloud'], classifiers=[ diff --git a/tools/requirements.txt b/tools/requirements.txt index 68582aec9..bd6848eaa 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -4,6 +4,6 @@ requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 -rich == 13.7.1 +rich == 13.8.0 # only used for soap transport # softlayer-zeep >= 5.0.0 From 007caa133091df354bcf0897a686bb4dd2bea456 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 21:39:43 +0000 Subject: [PATCH 33/77] pip prod(deps): bump rich from 13.8.0 to 13.8.1 Bumps [rich](https://github.com/Textualize/rich) from 13.8.0 to 13.8.1. - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v13.8.0...v13.8.1) --- updated-dependencies: - dependency-name: rich dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- setup.py | 2 +- tools/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 204759dff..6bc1abe7b 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ 'prompt_toolkit >= 2', 'pygments >= 2.0.0', 'urllib3 >= 1.24', - 'rich == 13.8.0' + 'rich == 13.8.1' ], keywords=['softlayer', 'cloud', 'slcli', 'ibmcloud'], classifiers=[ diff --git a/tools/requirements.txt b/tools/requirements.txt index bd6848eaa..3586c2f52 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -4,6 +4,6 @@ requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 -rich == 13.8.0 +rich == 13.8.1 # only used for soap transport # softlayer-zeep >= 5.0.0 From 1dd4f56f9498eb987b5e475205989e86a88c0431 Mon Sep 17 00:00:00 2001 From: kz6fittycent Date: Wed, 25 Sep 2024 09:49:18 -0500 Subject: [PATCH 34/77] core24 - workflow -snap builds - README update --- .github/workflows/test-snap-can-build.yml | 28 +++++++++++++++++++ README.rst | 3 ++- snap/local/slcli.png | Bin 0 -> 2442 bytes snap/snapcraft.yaml | 31 ++++++++++++++++++---- 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/test-snap-can-build.yml create mode 100644 snap/local/slcli.png diff --git a/.github/workflows/test-snap-can-build.yml b/.github/workflows/test-snap-can-build.yml new file mode 100644 index 000000000..19a4086bb --- /dev/null +++ b/.github/workflows/test-snap-can-build.yml @@ -0,0 +1,28 @@ +name: Snap Builds + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + + - uses: snapcore/action-build@v1 + id: build + + - uses: diddlesnaps/snapcraft-review-action@v1 + with: + snap: ${{ steps.build.outputs.snap }} + isClassic: 'false' + # Plugs and Slots declarations to override default denial (requires store assertion to publish) + # plugs: ./plug-declaration.json + # slots: ./slot-declaration.json diff --git a/README.rst b/README.rst index 39b0dfd4a..5f82bdd62 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,8 @@ SoftLayer API Python Client :target: https://coveralls.io/github/softlayer/softlayer-python?branch=master .. image:: https://snapcraft.io//slcli/badge.svg :target: https://snapcraft.io/slcli - +.. image:: https://https://github.com/softlayer/softlayer-python/workflows/Snap%20Builds/badge.svg + :target: https://github.com/softlayer/softlayer-python/actions?query=workflow:"Snap+Builds" This library provides a simple Python client to interact with `SoftLayer's XML-RPC API `_. diff --git a/snap/local/slcli.png b/snap/local/slcli.png new file mode 100644 index 0000000000000000000000000000000000000000..5e273f36e2371d151990f5fb14f905ed847f2921 GIT binary patch literal 2442 zcmb_dc~Fx{7Vj?v5<0FWcPI{5$q!6F3EQt%Z(eh>yfZbUdbdJ`QTHR8yzVG%!s0$`AvmTT+UdmP&z z=v(#8X_WN?0`ck&Or(1onZ1ZqLs`roRet=S(5>Nix*sJ7*dDy;6_#(R zPrTBM4dkz_ZyEPAovYZ5r_^CGA-mk9d`z`RmxB!}Lamg4%hB-Lx;|cCu-=6`JGslZ z2V+a)jb&}jL-dq4`ZR+~C;Qx#o4RV1yp$NHJ!(=l7atELXEvQNY!ccEWHT^np+(CU z48>lpd<};i*Ue-MdQRLpQ?_4L`fX0@n!*$6tpJ%08PYHQ$E6R2Oe%;_?9P^-?kQjN zF_CFz6GjHnic^x22L=Up38inPxiqOqn>X2~B*W~84HnI7N-sO*0s@X%getVpK*EZB zZ}KiY^YZ;UES+yIthRWWC`(}`v&Q*Xgg~dZZbvRI4xP1RkizQkv@TBB<-4=kI43MQ zuUSDD0+&T9=%kAi5P#kGSvB`yjdYyr*?0hqwZ9%HsmeY9R-!0G4`(71ACM(1pZOXbgZ(5Qa0&FohHdrD%Mo;(67u^J8jN0`m_ z!^&%;;?nXWW16rR7e3&MvBhYIX6q1__BFPa1eQm-MdO)zEisyf=0%Q3(iX5`8ZDmk z%^u8~o~|6vGrimGfqVu0kAc;#+}9~?%lH<(n&Q{zUkgQsL#VBfProx1WB1xTO>j{gaPEB-j3!YSXc z3Q}?^hodrdJNSZm2dS<=lX?uDp=6+|tNU)y0gzsR0Qv3j zkPIE|4Q%>;6fR$m^CJ&e{V(64+G-n1Jyz9bZdQ5LhX_!QrSt8C22~T$W!ZR^ewxlg z7ZOa!adt;5Dip-{MT*-sFN_|Tat7sJgnq zP~|?h6^Al7yz|sf!aG24sRlAwREO49*o`FsAi$@rcW5L6ph06(IM@d6fc{mB1{(h- z;nU2EW>n(>c)ly0w37%|ws`_eor(gHOul?Knv;!)vpL+rn44#mNpi>Q{3bK(2O6~- zYe4FHzbYm;WTZoqH1b@h=$XKrG!9Zl0v;xYMGPu8|nwHA+k$eS~UepNsKLsfcg zx4u#4n})8n)f&B zqdQA@yAkovw$~Z5{sfSN6YDT~HLI{bZp2kv7C+0>Y+#K5&d77dYP@`-PBLpsJ!|5D zF@q-awE^|L$}1=GC+65vq^#VzZy|30=WVpy_GHw`lWv?ie2F3pPNKMDUbK6bJXRg` z{g6T@nxkKlN><-$GiR%R>8y~*l;SSr*tg0yPJ*ta^&Vr+MTxa9g7{95BFAY8AN_>* z^d0Qh?;mDnW~LNkp{Aoq^$LkBh(IS zIW#a7?z`WTDC%|hF?LV{{uD&J8a;7#BNNoC(HGhp+Lrf9c}29jqCH#WjVg(?rR9cu zrXvuc;dve&G*ZFEZg-(2V7-%dSU1zCSEsLvgl$HE6)537;JRc2I3J<*y^qV4ZFvdu zoP#_t!_oy4`#8e|Fwk3cnK6t&K*B);svs2R*sVPB!$~~XO1fQvepTY7Nc7DjSqF{s9uO`tu1QEK#)VoDyE<*?LIr4a>K1_pCnqO%< zt(3fR*?NcJ`1%|?Hdcuh_!BgEisT%&_mpSk`S|$Uofu)jNSuK{kacaq)eh|&0#7(bC|Td#eVxG@3PU3+|C>-@Bq$v`rq5FbA Date: Mon, 30 Sep 2024 10:18:32 -0500 Subject: [PATCH 35/77] Setting pylint to ignore too-many-positional-arguments errors. Fixed #2186 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 63e8ac7fc..e0badfd06 100644 --- a/tox.ini +++ b/tox.ini @@ -52,6 +52,7 @@ commands = pylint SoftLayer/fixtures \ -d invalid-name \ -d missing-docstring \ + -d too-many-positional-arguments \ --max-module-lines=2000 \ --min-similarity-lines=50 \ --max-line-length=120 \ From a58f0d37bf03d2bb53e36471b6629f7241a3b6d4 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 30 Sep 2024 12:17:53 -0500 Subject: [PATCH 36/77] Setting pylint to ignore too-many-positional-arguments errors. Fixed #2186 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index e0badfd06..fccc3fbc7 100644 --- a/tox.ini +++ b/tox.ini @@ -39,6 +39,7 @@ commands = -d consider-using-dict-comprehension \ -d useless-import-alias \ -d consider-using-f-string \ + -d too-many-positional-arguments \ --max-args=25 \ --max-branches=20 \ --max-statements=65 \ From 174f8c1f2e1280b6c9b1aeecfeb5383bcb0de2c9 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 2 Oct 2024 17:11:46 -0500 Subject: [PATCH 37/77] Fixed an issues with search command. Fixed #2130 --- SoftLayer/CLI/search.py | 5 +++-- tests/CLI/modules/search_tests.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/search.py b/SoftLayer/CLI/search.py index 23329ce51..84a79ff6e 100644 --- a/SoftLayer/CLI/search.py +++ b/SoftLayer/CLI/search.py @@ -37,13 +37,14 @@ def cli(env, query, types, advanced): slcli -vvv search _objectType:SoftLayer_Hardware hostname:testibm --advanced """ - # Before any Search operation + # Checks to make sure we have at least 1 query. def check_opt(list_opt=None): check = False for input_ in list_opt: - if input_ is True: + if input_: check = True break + return check list_opt = [query, types, advanced] diff --git a/tests/CLI/modules/search_tests.py b/tests/CLI/modules/search_tests.py index c14ce6b84..1b2540779 100644 --- a/tests/CLI/modules/search_tests.py +++ b/tests/CLI/modules/search_tests.py @@ -1,6 +1,6 @@ """ - SoftLayer.tests.CLI.modules.find_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SoftLayer.tests.CLI.modules.search_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :license: MIT, see LICENSE for more details. """ @@ -8,16 +8,23 @@ from SoftLayer import testing -class FindTests(testing.TestCase): +class SearchTests(testing.TestCase): def test_find(self): result = self.run_command(['search', '--types']) + self.assert_called_with("SoftLayer_Search", "getObjectTypes") self.assert_no_fail(result) def test_find_advanced(self): result = self.run_command(['search', 'hardware', '--advanced']) + self.assert_called_with("SoftLayer_Search", "advancedSearch", args=('hardware',)) self.assert_no_fail(result) def test_no_options(self): result = self.run_command(['search']) self.assertEqual(result.exit_code, 2) + + def test_find_single_item(self): + result = self.run_command(['search', 'test.com']) + self.assert_no_fail(result) + self.assert_called_with("SoftLayer_Search", "search", args=('test.com',)) From 0e1d28fd3f0d39f0fe61860a0fe114a70819669f Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 3 Oct 2024 17:23:24 -0500 Subject: [PATCH 38/77] Added --owner, --public_ip and --private_ip search fields for hardware list, added owner and tags to the default output. Fixed #2037 --- SoftLayer/CLI/hardware/list.py | 43 ++++++--- SoftLayer/managers/hardware.py | 22 ++--- .../modules/hardware/hardware_basic_tests.py | 52 ----------- .../modules/hardware/hardware_list_tests.py | 93 +++++++++++++++++++ 4 files changed, 134 insertions(+), 76 deletions(-) create mode 100644 tests/CLI/modules/hardware/hardware_list_tests.py diff --git a/SoftLayer/CLI/hardware/list.py b/SoftLayer/CLI/hardware/list.py index 734f379d4..65a95718e 100644 --- a/SoftLayer/CLI/hardware/list.py +++ b/SoftLayer/CLI/hardware/list.py @@ -22,7 +22,7 @@ lambda server: formatting.active_txn(server), mask='activeTransaction[id, transactionStatus[name, friendlyName]]'), column_helper.Column( - 'created_by', + 'owner', lambda created_by: utils.lookup(created_by, 'billingItem', 'orderItem', 'order', 'userRecord', 'username'), mask='billingItem[id,orderItem[id,order[id,userRecord[username]]]]'), column_helper.Column( @@ -38,6 +38,8 @@ 'backend_ip', 'datacenter', 'action', + 'owner', + 'tags', ] @@ -48,6 +50,9 @@ @click.option('--hostname', '-H', help='Filter by hostname') @click.option('--memory', '-m', help='Filter by memory in gigabytes') @click.option('--network', '-n', help='Filter by network port speed in Mbps') +@click.option('--owner', help='Filter by created_by username') +@click.option('--primary_ip', help='Filter by Primary Ip Address') +@click.option('--backend_ip', help='Filter by Backend Ip Address') @click.option('--search', is_flag=False, flag_value="", default=None, help="Use the more flexible Search API to list instances. See `slcli search --types` for list " + "of searchable fields.") @@ -63,29 +68,41 @@ default=100, show_default=True) @environment.pass_env -def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, search, tag, columns, limit): +def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, owner, primary_ip, backend_ip, + search, tag, columns, limit): """List hardware servers.""" if search is not None: object_mask = "mask[resource(SoftLayer_Hardware)]" search_manager = SoftLayer.SearchManager(env.client) - servers = search_manager.search_hadrware_instances(hostname=hostname, domain=domain, datacenter=datacenter, - tags=tag, search_string=search, mask=object_mask) + servers = search_manager.search_hadrware_instances( + hostname=hostname, + domain=domain, + datacenter=datacenter, + tags=tag, + search_string=search, + mask=object_mask) else: manager = SoftLayer.HardwareManager(env.client) - servers = manager.list_hardware(hostname=hostname, - domain=domain, - cpus=cpu, - memory=memory, - datacenter=datacenter, - nic_speed=network, - tags=tag, - mask="mask(SoftLayer_Hardware_Server)[%s]" % columns.mask(), - limit=limit) + servers = manager.list_hardware( + hostname=hostname, + domain=domain, + cpus=cpu, + memory=memory, + datacenter=datacenter, + nic_speed=network, + tags=tag, + owner=owner, + public_ip=primary_ip, + private_ip=backend_ip, + mask="mask(SoftLayer_Hardware_Server)[%s]" % columns.mask(), + limit=limit) table = formatting.Table(columns.columns) table.sortby = sortby + table.align['created_by'] = 'l' + table.align['tags'] = 'l' for server in servers: table.add_row([value or formatting.blank() diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 6564fda0f..f791ec2f4 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -121,7 +121,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', immediate= @retry(logger=LOGGER) def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, - domain=None, datacenter=None, nic_speed=None, + domain=None, datacenter=None, nic_speed=None, owner=None, public_ip=None, private_ip=None, **kwargs): """List all hardware (servers and bare metal computing instances). @@ -169,6 +169,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, % (','.join(hw_items), ','.join(server_items))) _filter = utils.NestedDict(kwargs.get('filter') or {}) + _filter['id'] = utils.query_filter_orderby() if tags: _filter['hardware']['tagReferences']['tag']['name'] = { 'operation': 'in', @@ -176,8 +177,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, } if cpus: - _filter['hardware']['processorPhysicalCoreAmount'] = ( - utils.query_filter(cpus)) + _filter['hardware']['processorPhysicalCoreAmount'] = utils.query_filter(cpus) if memory: _filter['hardware']['memoryCapacity'] = utils.query_filter(memory) @@ -189,20 +189,20 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, _filter['hardware']['domain'] = utils.query_filter(domain) if datacenter: - _filter['hardware']['datacenter']['name'] = ( - utils.query_filter(datacenter)) + _filter['hardware']['datacenter']['name'] = utils.query_filter(datacenter) if nic_speed: - _filter['hardware']['networkComponents']['maxSpeed'] = ( - utils.query_filter(nic_speed)) + _filter['hardware']['networkComponents']['maxSpeed'] = utils.query_filter(nic_speed) if public_ip: - _filter['hardware']['primaryIpAddress'] = ( - utils.query_filter(public_ip)) + _filter['hardware']['primaryIpAddress'] = utils.query_filter(public_ip) if private_ip: - _filter['hardware']['primaryBackendIpAddress'] = ( - utils.query_filter(private_ip)) + _filter['hardware']['primaryBackendIpAddress'] = utils.query_filter(private_ip) + + if owner: + _filter['hardware']['billingItem']['orderItem']['order']['userRecord']['username'] = ( + utils.query_filter(owner)) kwargs['filter'] = _filter.to_dict() kwargs['iter'] = True diff --git a/tests/CLI/modules/hardware/hardware_basic_tests.py b/tests/CLI/modules/hardware/hardware_basic_tests.py index d7c2ca9b3..a5597872e 100644 --- a/tests/CLI/modules/hardware/hardware_basic_tests.py +++ b/tests/CLI/modules/hardware/hardware_basic_tests.py @@ -169,47 +169,6 @@ def test_detail_drives(self): self.assertEqual(output['drives'][0]['Name'], 'Seagate Constellation ES') self.assertEqual(output['drives'][0]['Serial #'], 'z1w4sdf') - def test_list_servers(self): - result = self.run_command(['server', 'list', '--tag=openstack']) - - expected = [ - { - 'datacenter': 'TEST00', - 'primary_ip': '172.16.1.100', - 'hostname': 'hardware-test1', - 'id': 1000, - 'backend_ip': '10.1.0.2', - 'action': 'TXN_NAME', - }, - { - 'datacenter': 'TEST00', - 'primary_ip': '172.16.4.94', - 'hostname': 'hardware-test2', - 'id': 1001, - 'backend_ip': '10.1.0.3', - 'action': None, - }, - { - 'datacenter': 'TEST00', - 'primary_ip': '172.16.4.95', - 'hostname': 'hardware-bad-memory', - 'id': 1002, - 'backend_ip': '10.1.0.4', - 'action': None, - }, - { - 'action': None, - 'backend_ip': None, - 'datacenter': None, - 'hostname': None, - 'id': 1003, - 'primary_ip': None, - }, - ] - - self.assert_no_fail(result) - self.assertEqual(expected, json.loads(result.output)) - @mock.patch('SoftLayer.CLI.formatting.no_going_back') @mock.patch('SoftLayer.HardwareManager.reload') def test_server_reload(self, reload_mock, ngb_mock): @@ -992,17 +951,6 @@ def test_create_credential(self): '--notes', 'test slcli', '--software', 'system']) self.assert_no_fail(result) - def test_list_hw_search_noargs(self): - result = self.run_command(['hw', 'list', '--search']) - self.assert_no_fail(result) - self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=('_objectType:SoftLayer_Hardware ',)) - - def test_list_hw_search_noargs_domain(self): - result = self.run_command(['hw', 'list', '--search', '-Dtest']) - self.assert_no_fail(result) - self.assert_called_with('SoftLayer_Search', 'advancedSearch', - args=('_objectType:SoftLayer_Hardware domain: *test*',)) - @mock.patch('SoftLayer.CLI.formatting.confirm') def test_hardware_cancel_no_force(self, confirm_mock): confirm_mock.return_value = False diff --git a/tests/CLI/modules/hardware/hardware_list_tests.py b/tests/CLI/modules/hardware/hardware_list_tests.py new file mode 100644 index 000000000..6ef091eba --- /dev/null +++ b/tests/CLI/modules/hardware/hardware_list_tests.py @@ -0,0 +1,93 @@ +""" + SoftLayer.tests.CLI.modules.hardware.hardware_list_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + These tests the `slcli hw list` command. Its complex enough to warrant its own file + + :license: MIT, see LICENSE for more details. +""" + +import json + +from SoftLayer import testing +from SoftLayer import utils + + +class HardwareListCLITests(testing.TestCase): + def test_list_servers(self): + colums = 'datacenter,primary_ip,hostname,id,backend_ip,action' + result = self.run_command(['server', 'list', '--tag=openstack', f'--columns={colums}']) + + expected = [ + { + 'datacenter': 'TEST00', + 'primary_ip': '172.16.1.100', + 'hostname': 'hardware-test1', + 'id': 1000, + 'backend_ip': '10.1.0.2', + 'action': 'TXN_NAME', + }, + { + 'datacenter': 'TEST00', + 'primary_ip': '172.16.4.94', + 'hostname': 'hardware-test2', + 'id': 1001, + 'backend_ip': '10.1.0.3', + 'action': None, + }, + { + 'datacenter': 'TEST00', + 'primary_ip': '172.16.4.95', + 'hostname': 'hardware-bad-memory', + 'id': 1002, + 'backend_ip': '10.1.0.4', + 'action': None, + }, + { + 'action': None, + 'backend_ip': None, + 'datacenter': None, + 'hostname': None, + 'id': 1003, + 'primary_ip': None, + }, + ] + + self.assert_no_fail(result) + self.assertEqual(expected, json.loads(result.output)) + + def test_list_hw_search_noargs(self): + result = self.run_command(['hw', 'list', '--search']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=('_objectType:SoftLayer_Hardware ',)) + + def test_list_hw_search_noargs_domain(self): + result = self.run_command(['hw', 'list', '--search', '-Dtest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Search', 'advancedSearch', + args=('_objectType:SoftLayer_Hardware domain: *test*',)) + + def test_list_by_owner(self): + result = self.run_command(['hw', 'list', '--owner=testUser']) + self.assert_no_fail(result) + expectedFilter = utils.NestedDict() + expectedFilter['id'] = utils.query_filter_orderby() + expectedFilter['hardware']['billingItem']['orderItem']['order']['userRecord']['username'] = ( + utils.query_filter('testUser')) + self.assert_called_with('SoftLayer_Account', 'getHardware', filter=expectedFilter) + + def test_list_by_pub_ip(self): + result = self.run_command(['hw', 'list', '--primary_ip=1.2.3.4']) + self.assert_no_fail(result) + expectedFilter = utils.NestedDict() + expectedFilter['id'] = utils.query_filter_orderby() + expectedFilter['hardware']['primaryIpAddress'] = utils.query_filter('1.2.3.4') + self.assert_called_with('SoftLayer_Account', 'getHardware', filter=expectedFilter) + + def test_list_by_pri_ip(self): + result = self.run_command(['hw', 'list', '--backend_ip=1.2.3.4']) + self.assert_no_fail(result) + expectedFilter = utils.NestedDict() + expectedFilter['id'] = utils.query_filter_orderby() + expectedFilter['hardware']['primaryBackendIpAddress'] = utils.query_filter('1.2.3.4') + self.assert_called_with('SoftLayer_Account', 'getHardware', filter=expectedFilter) From 160e7a2be673e816034a3f4918c1281d52d92915 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 3 Oct 2024 17:34:10 -0500 Subject: [PATCH 39/77] Fixed filtering test --- SoftLayer/managers/hardware.py | 2 +- tests/managers/hardware_tests.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index f791ec2f4..c8dea24b4 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -169,7 +169,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, % (','.join(hw_items), ','.join(server_items))) _filter = utils.NestedDict(kwargs.get('filter') or {}) - _filter['id'] = utils.query_filter_orderby() + _filter['hardware']['id'] = utils.query_filter_orderby() if tags: _filter['hardware']['tagReferences']['tag']['name'] = { 'operation': 'in', diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index c067dd084..12870a43f 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -57,6 +57,7 @@ def test_list_hardware_with_filters(self): self.assertEqual(results, fixtures.SoftLayer_Account.getHardware) _filter = { 'hardware': { + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}, 'datacenter': {'name': {'operation': '_= dal05'}}, 'domain': {'operation': '_= example.com'}, 'tagReferences': { @@ -73,8 +74,7 @@ def test_list_hardware_with_filters(self): 'networkComponents': {'maxSpeed': {'operation': 100}}, 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'}} } - self.assert_called_with('SoftLayer_Account', 'getHardware', - filter=_filter) + self.assert_called_with('SoftLayer_Account', 'getHardware', filter=_filter) def test_resolve_ids_ip(self): _id = self.hardware._get_ids_from_ip('172.16.1.100') From 8aeb2ce1fe58d73ab33fc81c7f004b227898047b Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 3 Oct 2024 17:54:52 -0500 Subject: [PATCH 40/77] Fixed unit tests again --- tests/CLI/modules/hardware/hardware_list_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CLI/modules/hardware/hardware_list_tests.py b/tests/CLI/modules/hardware/hardware_list_tests.py index 6ef091eba..1532da60c 100644 --- a/tests/CLI/modules/hardware/hardware_list_tests.py +++ b/tests/CLI/modules/hardware/hardware_list_tests.py @@ -71,7 +71,7 @@ def test_list_by_owner(self): result = self.run_command(['hw', 'list', '--owner=testUser']) self.assert_no_fail(result) expectedFilter = utils.NestedDict() - expectedFilter['id'] = utils.query_filter_orderby() + expectedFilter['hardware']['id'] = utils.query_filter_orderby() expectedFilter['hardware']['billingItem']['orderItem']['order']['userRecord']['username'] = ( utils.query_filter('testUser')) self.assert_called_with('SoftLayer_Account', 'getHardware', filter=expectedFilter) @@ -80,7 +80,7 @@ def test_list_by_pub_ip(self): result = self.run_command(['hw', 'list', '--primary_ip=1.2.3.4']) self.assert_no_fail(result) expectedFilter = utils.NestedDict() - expectedFilter['id'] = utils.query_filter_orderby() + expectedFilter['hardware']['id'] = utils.query_filter_orderby() expectedFilter['hardware']['primaryIpAddress'] = utils.query_filter('1.2.3.4') self.assert_called_with('SoftLayer_Account', 'getHardware', filter=expectedFilter) @@ -88,6 +88,6 @@ def test_list_by_pri_ip(self): result = self.run_command(['hw', 'list', '--backend_ip=1.2.3.4']) self.assert_no_fail(result) expectedFilter = utils.NestedDict() - expectedFilter['id'] = utils.query_filter_orderby() + expectedFilter['hardware']['id'] = utils.query_filter_orderby() expectedFilter['hardware']['primaryBackendIpAddress'] = utils.query_filter('1.2.3.4') self.assert_called_with('SoftLayer_Account', 'getHardware', filter=expectedFilter) From 1d64f954d66000909b73ab3de6f19965f392a201 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 4 Oct 2024 17:18:19 -0500 Subject: [PATCH 41/77] Added get_total_items to SoftLayerListResult to make figuring out how many results were returned a bit easier --- .secrets.baseline | 4 ++-- SoftLayer/transports/fixture.py | 7 ++++++- SoftLayer/transports/rest.py | 3 +-- SoftLayer/transports/transport.py | 4 ++++ SoftLayer/transports/xmlrpc.py | 3 +-- tests/transports/rest_tests.py | 1 + tests/transports/transport_tests.py | 7 +++++++ tests/transports/xmlrpc_tests.py | 1 + 8 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index ea850e071..9062c20d5 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2024-04-25T01:18:20Z", + "generated_at": "2024-10-04T22:18:14Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -720,7 +720,7 @@ "hashed_secret": "9878e362285eb314cfdbaa8ee8c300c285856810", "is_secret": false, "is_verified": false, - "line_number": 323, + "line_number": 324, "type": "Secret Keyword", "verified_result": null } diff --git a/SoftLayer/transports/fixture.py b/SoftLayer/transports/fixture.py index 6975e92b6..7a6b64e19 100644 --- a/SoftLayer/transports/fixture.py +++ b/SoftLayer/transports/fixture.py @@ -8,6 +8,8 @@ import importlib +from .transport import SoftLayerListResult + class FixtureTransport(object): """Implements a transport which returns fixtures.""" @@ -21,7 +23,10 @@ def __call__(self, call): message = f'{call.service} fixture is not implemented' raise NotImplementedError(message) from ex try: - return getattr(module, call.method) + result = getattr(module, call.method) + if isinstance(result, list): + return SoftLayerListResult(result, len(result)) + return result except AttributeError as ex: message = f'{call.service}::{call.method} fixture is not implemented' raise NotImplementedError(message) from ex diff --git a/SoftLayer/transports/rest.py b/SoftLayer/transports/rest.py index 30ce11bad..9d2a13269 100644 --- a/SoftLayer/transports/rest.py +++ b/SoftLayer/transports/rest.py @@ -138,8 +138,7 @@ def __call__(self, request): request.result = result if isinstance(result, list): - return SoftLayerListResult( - result, int(resp.headers.get('softlayer-total-items', 0))) + return SoftLayerListResult(result, int(resp.headers.get('softlayer-total-items', 0))) else: return result except requests.HTTPError as ex: diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index ab5ebedde..9496be50b 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -121,6 +121,10 @@ def __init__(self, items=None, total_count=0): self.total_count = total_count super().__init__(items) + def get_total_items(self): + """A simple getter to totalCount, but its called getTotalItems since that is the header returned""" + return self.total_count + def _proxies_dict(proxy): """Makes a proxy dict appropriate to pass to requests.""" diff --git a/SoftLayer/transports/xmlrpc.py b/SoftLayer/transports/xmlrpc.py index 57ba4e9f6..66cdb5707 100644 --- a/SoftLayer/transports/xmlrpc.py +++ b/SoftLayer/transports/xmlrpc.py @@ -100,8 +100,7 @@ def __call__(self, request): resp.raise_for_status() result = xmlrpc.client.loads(resp.content)[0][0] if isinstance(result, list): - return SoftLayerListResult( - result, int(resp.headers.get('softlayer-total-items', 0))) + return SoftLayerListResult(result, int(resp.headers.get('softlayer-total-items', 0))) else: return result except xmlrpc.client.Fault as ex: diff --git a/tests/transports/rest_tests.py b/tests/transports/rest_tests.py index 2c3d5f68f..20186e95f 100644 --- a/tests/transports/rest_tests.py +++ b/tests/transports/rest_tests.py @@ -37,6 +37,7 @@ def test_basic(self, request): self.assertEqual(resp, []) self.assertIsInstance(resp, transports.SoftLayerListResult) self.assertEqual(resp.total_count, 10) + self.assertEqual(resp.get_total_items(), 10) request.assert_called_with( 'GET', 'http://something9999999999999999999999.com/SoftLayer_Service/Resource.json', headers=mock.ANY, diff --git a/tests/transports/transport_tests.py b/tests/transports/transport_tests.py index c22d11b9d..e4f2d7280 100644 --- a/tests/transports/transport_tests.py +++ b/tests/transports/transport_tests.py @@ -20,6 +20,13 @@ def test_basic(self): resp = self.transport(req) self.assertEqual(resp['accountId'], 1234) + def test_total_items(self): + req = transports.Request() + req.service = 'SoftLayer_Account' + req.method = 'getHardware' + resp = self.transport(req) + self.assertEqual(resp.get_total_items(), 4) + def test_no_module(self): req = transports.Request() req.service = 'Doesnt_Exist' diff --git a/tests/transports/xmlrpc_tests.py b/tests/transports/xmlrpc_tests.py index 6e669279e..051e13822 100644 --- a/tests/transports/xmlrpc_tests.py +++ b/tests/transports/xmlrpc_tests.py @@ -409,6 +409,7 @@ def test_nonascii_characters(self, request): self.assertEqual(resp, []) self.assertIsInstance(resp, transports.SoftLayerListResult) self.assertEqual(resp.total_count, 10) + self.assertEqual(resp.get_total_items(), 10) @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') From 17c98788030cecfbf7080c3cb35967dcfd26aeac Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 7 Oct 2024 14:49:39 -0500 Subject: [PATCH 42/77] Removed the migrate filter for guests since it doesn't seem to work, now manually checking 'pendingMIgrationFlag'. Also added an 'id' orderBy to the guest_list manager --- SoftLayer/CLI/virt/migrate.py | 36 ++++++++++++++++++++--------------- SoftLayer/managers/vs.py | 1 + 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/SoftLayer/CLI/virt/migrate.py b/SoftLayer/CLI/virt/migrate.py index b4a1edef9..a184a8141 100644 --- a/SoftLayer/CLI/virt/migrate.py +++ b/SoftLayer/CLI/virt/migrate.py @@ -19,7 +19,6 @@ def cli(env, guest, migrate_all, host): """Manage VSIs that require migration. Can migrate Dedicated Host VSIs as well.""" vsi = SoftLayer.VSManager(env.client) - pending_filter = {'virtualGuests': {'pendingMigrationFlag': {'operation': 1}}} dedicated_filter = {'virtualGuests': {'dedicatedHost': {'id': {'operation': 'not null'}}}} mask = """mask[ id, hostname, domain, datacenter, pendingMigrationFlag, powerState, @@ -28,21 +27,22 @@ def cli(env, guest, migrate_all, host): # No options, just print out a list of guests that can be migrated if not (guest or migrate_all): - require_migration = vsi.list_instances(filter=pending_filter, mask=mask) + require_migration = vsi.list_instances(mask=mask) require_table = formatting.Table(['id', 'hostname', 'domain', 'datacenter'], title="Require Migration") for vsi_object in require_migration: - require_table.add_row([ - vsi_object.get('id'), - vsi_object.get('hostname'), - vsi_object.get('domain'), - utils.lookup(vsi_object, 'datacenter', 'name') - ]) + if vsi_object['pendingMigrationFlag']: + require_table.add_row([ + vsi_object.get('id'), + vsi_object.get('hostname'), + vsi_object.get('domain'), + utils.lookup(vsi_object, 'datacenter', 'name') + ]) - if require_migration: + if len(require_table.rows) > 0: env.fout(require_table) else: - click.secho("No guests require migration at this time", fg='green') + click.secho("No guests require migration at this time.", fg='green') migrateable = vsi.list_instances(filter=dedicated_filter, mask=mask) migrateable_table = formatting.Table(['id', 'hostname', 'domain', 'datacenter', 'Host Name', 'Host Id'], @@ -56,14 +56,20 @@ def cli(env, guest, migrate_all, host): utils.lookup(vsi_object, 'dedicatedHost', 'name'), utils.lookup(vsi_object, 'dedicatedHost', 'id') ]) - env.fout(migrateable_table) + if len(migrateable_table.rows) > 0: + env.fout(migrateable_table) + else: + click.secho("No dedicated guests to migrate.", fg='green') # Migrate all guests with pendingMigrationFlag=True elif migrate_all: - require_migration = vsi.list_instances(filter=pending_filter, mask="mask[id]") - if not require_migration: - click.secho("No guests require migration at this time", fg='green') + require_migration = vsi.list_instances(mask="mask[id,pendingMigrationFlag]") + migrated = 0 for vsi_object in require_migration: - migrate(vsi, vsi_object['id']) + if vsi_object['pendingMigrationFlag']: + migrated = migrated + 1 + migrate(vsi, vsi_object['id']) + if migrated == 0: + click.secho("No guests require migration at this time", fg='green') # Just migrate based on the options else: migrate(vsi, guest, host) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 534ea246e..dddc6310e 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -131,6 +131,7 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, call = 'getMonthlyVirtualGuests' _filter = utils.NestedDict(kwargs.get('filter') or {}) + _filter['virtualGuests']['id'] = utils.query_filter_orderby() if tags: _filter['virtualGuests']['tagReferences']['tag']['name'] = { 'operation': 'in', From 9663ba8fe0f9292d0a7bddabdaa5ded8cf6264f5 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 7 Oct 2024 15:11:00 -0500 Subject: [PATCH 43/77] Fixed unit test error --- SoftLayer/testing/xmlrpc.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/SoftLayer/testing/xmlrpc.py b/SoftLayer/testing/xmlrpc.py index b60c2bf0c..a572fd79d 100644 --- a/SoftLayer/testing/xmlrpc.py +++ b/SoftLayer/testing/xmlrpc.py @@ -45,18 +45,17 @@ def do_POST(self): req.args = args[1:] req.filter = _item_by_key_postfix(headers, 'ObjectFilter') or None req.mask = _item_by_key_postfix(headers, 'ObjectMask').get('mask') - req.identifier = _item_by_key_postfix(headers, - 'InitParameters').get('id') - req.transport_headers = dict(((k.lower(), v) - for k, v in self.headers.items())) + req.identifier = _item_by_key_postfix(headers, 'InitParameters').get('id') + req.transport_headers = dict(((k.lower(), v) for k, v in self.headers.items())) req.headers = headers # Get response response = self.server.transport(req) - response_body = xmlrpc.client.dumps((response,), - allow_none=True, - methodresponse=True) + # Need to convert BACK to list, so xmlrpc can dump it out properly. + if isinstance(response, SoftLayer.transports.transport.SoftLayerListResult): + response = list(response) + response_body = xmlrpc.client.dumps((response,), allow_none=True, methodresponse=True) self.send_response(200) self.send_header("Content-type", "application/xml; charset=UTF-8") @@ -70,18 +69,14 @@ def do_POST(self): self.send_response(200) self.end_headers() response = xmlrpc.client.Fault(404, str(ex)) - response_body = xmlrpc.client.dumps(response, - allow_none=True, - methodresponse=True) + response_body = xmlrpc.client.dumps(response, allow_none=True, methodresponse=True) self.wfile.write(response_body.encode('utf-8')) except SoftLayer.SoftLayerAPIError as ex: self.send_response(200) self.end_headers() response = xmlrpc.client.Fault(ex.faultCode, str(ex.reason)) - response_body = xmlrpc.client.dumps(response, - allow_none=True, - methodresponse=True) + response_body = xmlrpc.client.dumps(response, allow_none=True, methodresponse=True) self.wfile.write(response_body.encode('utf-8')) except Exception: self.send_response(500) @@ -103,7 +98,6 @@ def _item_by_key_postfix(dictionary, key_prefix): def create_test_server(transport, host='localhost', port=0): """Create a test XML-RPC server in a new thread.""" server = TestServer(transport, (host, port), TestHandler) - thread = threading.Thread(target=server.serve_forever, - kwargs={'poll_interval': 0.01}) + thread = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': 0.01}) thread.start() return server From b41158b46cf0386e36fd029ea3942edb0167fb55 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 7 Oct 2024 16:05:10 -0500 Subject: [PATCH 44/77] Fixed vs migrate unit tests. Fixed #2123 --- .secrets.baseline | 6 +++--- SoftLayer/CLI/virt/migrate.py | 4 ++-- SoftLayer/fixtures/SoftLayer_Account.py | 3 ++- tests/managers/vs/vs_tests.py | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 9062c20d5..b256d881d 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2024-10-04T22:18:14Z", + "generated_at": "2024-10-07T21:05:06Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -112,7 +112,7 @@ "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", "is_secret": false, "is_verified": false, - "line_number": 121, + "line_number": 122, "type": "Secret Keyword", "verified_result": null }, @@ -120,7 +120,7 @@ "hashed_secret": "df51e37c269aa94d38f93e537bf6e2020b21406c", "is_secret": false, "is_verified": false, - "line_number": 1035, + "line_number": 1036, "type": "Secret Keyword", "verified_result": null } diff --git a/SoftLayer/CLI/virt/migrate.py b/SoftLayer/CLI/virt/migrate.py index a184a8141..c1a1028bf 100644 --- a/SoftLayer/CLI/virt/migrate.py +++ b/SoftLayer/CLI/virt/migrate.py @@ -31,7 +31,7 @@ def cli(env, guest, migrate_all, host): require_table = formatting.Table(['id', 'hostname', 'domain', 'datacenter'], title="Require Migration") for vsi_object in require_migration: - if vsi_object['pendingMigrationFlag']: + if vsi_object.get('pendingMigrationFlag', False): require_table.add_row([ vsi_object.get('id'), vsi_object.get('hostname'), @@ -65,7 +65,7 @@ def cli(env, guest, migrate_all, host): require_migration = vsi.list_instances(mask="mask[id,pendingMigrationFlag]") migrated = 0 for vsi_object in require_migration: - if vsi_object['pendingMigrationFlag']: + if vsi_object.get('pendingMigrationFlag', False): migrated = migrated + 1 migrate(vsi, vsi_object['id']) if migrated == 0: diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 96a1a0ee8..a8f1ff71c 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -35,7 +35,7 @@ 'globalIdentifier': '1a2b3c-1701', 'primaryBackendIpAddress': '10.45.19.37', 'hourlyBillingFlag': False, - + 'pendingMigrationFlag': True, 'billingItem': { 'id': 6327, 'recurringFee': 1.54, @@ -63,6 +63,7 @@ 'globalIdentifier': '05a8ac-6abf0', 'primaryBackendIpAddress': '10.45.19.35', 'hourlyBillingFlag': True, + 'pendingMigrationFlag': True, 'billingItem': { 'id': 6327, 'recurringFee': 1.54, diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py index a0ac6dae8..7d644630c 100644 --- a/tests/managers/vs/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -65,6 +65,7 @@ def test_list_instances_with_filters(self): _filter = { 'virtualGuests': { + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}, 'datacenter': { 'name': {'operation': '_= dal05'}}, 'domain': {'operation': '_= example.com'}, @@ -83,8 +84,7 @@ def test_list_instances_with_filters(self): 'transientGuestFlag': {'operation': False}, } } - self.assert_called_with('SoftLayer_Account', 'getVirtualGuests', - filter=_filter) + self.assert_called_with('SoftLayer_Account', 'getVirtualGuests', filter=_filter) def test_resolve_ids_ip(self): _id = self.vs._get_ids_from_ip('172.16.240.2') From ff61b19d9c6240bacfdacc615fa68f6dfc8656d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:49:19 +0000 Subject: [PATCH 45/77] pip prod(deps): bump sphinx-rtd-theme from 2.0.0 to 3.0.0 Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 2.0.0 to 3.0.0. - [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst) - [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/2.0.0...3.0.0) --- updated-dependencies: - dependency-name: sphinx-rtd-theme dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2c1e4075b..19e0212bf 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ sphinx==8.0.2 -sphinx_rtd_theme==2.0.0 +sphinx_rtd_theme==3.0.0 sphinx-click==6.0.0 click prettytable From d5023698aeda535a65c6753bd168aa9a59961f62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:49:23 +0000 Subject: [PATCH 46/77] pip prod(deps): bump rich from 13.8.1 to 13.9.2 Bumps [rich](https://github.com/Textualize/rich) from 13.8.1 to 13.9.2. - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v13.8.1...v13.9.2) --- updated-dependencies: - dependency-name: rich dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- setup.py | 2 +- tools/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6bc1abe7b..54989f14c 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ 'prompt_toolkit >= 2', 'pygments >= 2.0.0', 'urllib3 >= 1.24', - 'rich == 13.8.1' + 'rich == 13.9.2' ], keywords=['softlayer', 'cloud', 'slcli', 'ibmcloud'], classifiers=[ diff --git a/tools/requirements.txt b/tools/requirements.txt index 3586c2f52..92ce6e708 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -4,6 +4,6 @@ requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 -rich == 13.8.1 +rich == 13.9.2 # only used for soap transport # softlayer-zeep >= 5.0.0 From 28c72c8586dc275831f460200746ddeb8cce79de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:36:16 +0000 Subject: [PATCH 47/77] pip prod(deps): bump sphinx from 8.0.2 to 8.1.3 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.0.2 to 8.1.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.0.2...v8.1.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 19e0212bf..d8f3620a8 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx==8.0.2 +sphinx==8.1.3 sphinx_rtd_theme==3.0.0 sphinx-click==6.0.0 click From f3f21ab429516dfed35fd7ec4fcf71ea1e3d1e6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:36:21 +0000 Subject: [PATCH 48/77] pip prod(deps): bump sphinx-rtd-theme from 3.0.0 to 3.0.1 Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 3.0.0 to 3.0.1. - [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst) - [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/3.0.0...3.0.1) --- updated-dependencies: - dependency-name: sphinx-rtd-theme dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 19e0212bf..b48cc337d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ sphinx==8.0.2 -sphinx_rtd_theme==3.0.0 +sphinx_rtd_theme==3.0.1 sphinx-click==6.0.0 click prettytable From fbdf40ead46f6c0399053299c84df66860f0afa3 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 18 Oct 2024 14:03:02 -0500 Subject: [PATCH 49/77] Allow for proper debugging of config setup --- SoftLayer/CLI/config/setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SoftLayer/CLI/config/setup.py b/SoftLayer/CLI/config/setup.py index db895fa32..3dc15854c 100644 --- a/SoftLayer/CLI/config/setup.py +++ b/SoftLayer/CLI/config/setup.py @@ -78,6 +78,8 @@ def cli(env, auth): username = 'apikey' secret = env.getpass('Classic Infrastructure API Key', default=defaults['api_key']) new_client = SoftLayer.Client(username=username, api_key=secret, endpoint_url=endpoint_url, timeout=timeout) + env.client = new_client + env.client.transport = SoftLayer.DebugTransport(new_client.transport) api_key = get_api_key(new_client, username, secret) elif auth == 'sso': @@ -87,6 +89,8 @@ def cli(env, auth): username = env.input('Classic Infrastructure Username', default=defaults['username']) secret = env.getpass('Classic Infrastructure API Key', default=defaults['api_key']) new_client = SoftLayer.Client(username=username, api_key=secret, endpoint_url=endpoint_url, timeout=timeout) + env.client = new_client + env.client.transport = SoftLayer.DebugTransport(new_client.transport) api_key = get_api_key(new_client, username, secret) # Ask for timeout, convert to float, then to int From a64f6ea8268c189d4debef797be2ece94963a885 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:59:28 +0000 Subject: [PATCH 50/77] pip prod(deps): bump rich from 13.9.2 to 13.9.4 Bumps [rich](https://github.com/Textualize/rich) from 13.9.2 to 13.9.4. - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v13.9.2...v13.9.4) --- updated-dependencies: - dependency-name: rich dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- setup.py | 2 +- tools/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 54989f14c..e539fb5e2 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ 'prompt_toolkit >= 2', 'pygments >= 2.0.0', 'urllib3 >= 1.24', - 'rich == 13.9.2' + 'rich == 13.9.4' ], keywords=['softlayer', 'cloud', 'slcli', 'ibmcloud'], classifiers=[ diff --git a/tools/requirements.txt b/tools/requirements.txt index 92ce6e708..335a26b20 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -4,6 +4,6 @@ requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 -rich == 13.9.2 +rich == 13.9.4 # only used for soap transport # softlayer-zeep >= 5.0.0 From 4552db39cfd459cdd6767921aabcb2ee809c8ca0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:49:25 +0000 Subject: [PATCH 51/77] pip prod(deps): bump sphinx-rtd-theme from 3.0.1 to 3.0.2 Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 3.0.1 to 3.0.2. - [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst) - [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/3.0.1...3.0.2) --- updated-dependencies: - dependency-name: sphinx-rtd-theme dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index dd58003d1..118d14bbc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx_rtd_theme==3.0.1 +sphinx_rtd_theme==3.0.2 sphinx==8.1.3 sphinx-click==6.0.0 click From c1e743a02dbc90bf9b38313870b89aae227264c9 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 20 Nov 2024 12:00:56 -0600 Subject: [PATCH 52/77] Update README-internal.md --- README-internal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README-internal.md b/README-internal.md index 06e0a050e..df372ded0 100644 --- a/README-internal.md +++ b/README-internal.md @@ -12,7 +12,7 @@ security export -t certs -f pemseq -k /System/Library/Keychains/SystemRootCertif sudo cp bundleCA.pem /etc/ssl/certs/bundleCA.pem ``` Then in the `~/.softlayer` config, set `verify = /etc/ssl/certs/bundleCA.pem` and that should work. - +You may also need to set `REQUESTS_CA_BUNDLE` -> `export REQUESTS_CA_BUNDLE=/etc/ssl/certs/bundleCA.pem` to force python to load your CA bundle ## Certificate Example @@ -69,4 +69,4 @@ You can login and use the `slcli` with. Use the `-i` flag to make internal API c slcli -i emplogin ``` -If you want to use any of the built in commands, you may need to use the `-a ` flag. \ No newline at end of file +If you want to use any of the built in commands, you may need to use the `-a ` flag. From 87648057e0a091c7216e6e0281581f1dd217bdd9 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 13 Dec 2024 15:47:56 -0600 Subject: [PATCH 53/77] Added feature to iter_call to force a orderBy filter on the id property if the filter does not already include an orderBy filter of some kind. Updated all the methods that were using unOrdered iter_call to have a specific orderBy filter. --- SoftLayer/API.py | 2 + SoftLayer/CLI/environment.py | 1 + SoftLayer/managers/block.py | 10 ++- SoftLayer/managers/dns.py | 1 + SoftLayer/managers/event_log.py | 5 +- SoftLayer/managers/file.py | 2 +- SoftLayer/managers/image.py | 8 +-- SoftLayer/managers/network.py | 1 + SoftLayer/utils.py | 27 ++++++++ tests/CLI/modules/block_tests.py | 3 +- tests/CLI/modules/event_log_tests.py | 5 +- tests/CLI/modules/file_tests.py | 13 ++-- tests/api_tests.py | 35 +++++----- tests/managers/block_tests.py | 43 +++++-------- tests/managers/dedicated_host_tests.py | 3 +- tests/managers/dns_tests.py | 17 +++-- tests/managers/event_log_tests.py | 46 ++++++++++---- tests/managers/file_tests.py | 26 ++++---- tests/managers/image_tests.py | 7 +- tests/managers/network_tests.py | 11 +++- tests/managers/vs/vs_capacity_tests.py | 9 ++- tests/utils_tests.py | 88 ++++++++++++++++++++++++++ 22 files changed, 247 insertions(+), 116 deletions(-) create mode 100644 tests/utils_tests.py diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 00dbfe74f..ea119a4b1 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -19,6 +19,7 @@ from SoftLayer import consts from SoftLayer import exceptions from SoftLayer import transports +from SoftLayer import utils LOGGER = logging.getLogger(__name__) API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT @@ -403,6 +404,7 @@ def iter_call(self, service, method, *args, **kwargs): kwargs['iter'] = False result_count = 0 keep_looping = True + kwargs['filter'] = utils.fix_filter(kwargs.get('filter')) while keep_looping: # Get the next results diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index e2fde6e30..d5b8f584b 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -111,6 +111,7 @@ def getpass(self, prompt, default=None): # In windows, shift+insert actually inputs the below 2 characters # If we detect those 2 characters, need to manually read from the clipbaord instead # https://stackoverflow.com/questions/101128/how-do-i-read-text-from-the-clipboard + # LINUX NOTICE: `apt-get install python3-tk` required to install tk if password == 'àR': # tkinter is a built in python gui, but it has clipboard reading functions. # pylint: disable=import-outside-toplevel diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py index f7d0f1f11..5286d485f 100644 --- a/SoftLayer/managers/block.py +++ b/SoftLayer/managers/block.py @@ -53,23 +53,21 @@ def list_block_volumes(self, datacenter=None, username=None, storage_type=None, _filter = utils.NestedDict(kwargs.get('filter') or {}) _filter['iscsiNetworkStorage']['serviceResource']['type']['type'] = utils.query_filter('!~ ISCSI') + _filter['iscsiNetworkStorage']['id'] = utils.query_filter_orderby() - _filter['iscsiNetworkStorage']['storageType']['keyName'] = ( - utils.query_filter('*BLOCK_STORAGE*')) + _filter['iscsiNetworkStorage']['storageType']['keyName'] = utils.query_filter('*BLOCK_STORAGE*') if storage_type: _filter['iscsiNetworkStorage']['storageType']['keyName'] = ( utils.query_filter('%s_BLOCK_STORAGE*' % storage_type.upper())) if datacenter: - _filter['iscsiNetworkStorage']['serviceResource']['datacenter'][ - 'name'] = utils.query_filter(datacenter) + _filter['iscsiNetworkStorage']['serviceResource']['datacenter']['name'] = utils.query_filter(datacenter) if username: _filter['iscsiNetworkStorage']['username'] = utils.query_filter(username) if order: - _filter['iscsiNetworkStorage']['billingItem']['orderItem'][ - 'order']['id'] = utils.query_filter(order) + _filter['iscsiNetworkStorage']['billingItem']['orderItem']['order']['id'] = utils.query_filter(order) kwargs['filter'] = _filter.to_dict() return self.client.call('Account', 'getIscsiNetworkStorage', iter=True, **kwargs) diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index 6484fd7d9..db65528bf 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -196,6 +196,7 @@ def get_records(self, zone_id, ttl=None, data=None, host=None, record_type=None) :returns: A list of dictionaries representing the matching records within the specified zone. """ _filter = utils.NestedDict() + _filter['resourceRecords']['id'] = utils.query_filter_orderby() if ttl: _filter['resourceRecords']['ttl'] = utils.query_filter(ttl) diff --git a/SoftLayer/managers/event_log.py b/SoftLayer/managers/event_log.py index cc0a7f5cd..417b91409 100644 --- a/SoftLayer/managers/event_log.py +++ b/SoftLayer/managers/event_log.py @@ -65,11 +65,8 @@ def build_filter(date_min=None, date_max=None, obj_event=None, obj_id=None, obj_ :returns: dict: The generated query filter """ - - if not any([date_min, date_max, obj_event, obj_id, obj_type]): - return {} - request_filter = {} + request_filter['traceId'] = utils.query_filter_orderby() if date_min and date_max: request_filter['eventCreateDate'] = utils.event_log_filter_between_date(date_min, date_max, utc_offset) diff --git a/SoftLayer/managers/file.py b/SoftLayer/managers/file.py index d7e3871b9..0489e597c 100644 --- a/SoftLayer/managers/file.py +++ b/SoftLayer/managers/file.py @@ -47,7 +47,7 @@ def list_file_volumes(self, datacenter=None, username=None, storage_type=None, o kwargs['mask'] = ','.join(items) _filter = utils.NestedDict(kwargs.get('filter') or {}) - + _filter['nasNetworkStorage']['id'] = utils.query_filter_orderby() _filter['nasNetworkStorage']['serviceResource']['type']['type'] = utils.query_filter('!~ NAS') _filter['nasNetworkStorage']['storageType']['keyName'] = ( diff --git a/SoftLayer/managers/image.py b/SoftLayer/managers/image.py index f7f7005eb..84ab6665e 100644 --- a/SoftLayer/managers/image.py +++ b/SoftLayer/managers/image.py @@ -57,13 +57,12 @@ def list_private_images(self, guid=None, name=None, limit=100, **kwargs): kwargs['mask'] = IMAGE_MASK _filter = utils.NestedDict(kwargs.get('filter') or {}) + _filter['privateBlockDeviceTemplateGroups']['id'] = utils.query_filter_orderby() if name: - _filter['privateBlockDeviceTemplateGroups']['name'] = ( - utils.query_filter(name)) + _filter['privateBlockDeviceTemplateGroups']['name'] = utils.query_filter(name) if guid: - _filter['privateBlockDeviceTemplateGroups']['globalIdentifier'] = ( - utils.query_filter(guid)) + _filter['privateBlockDeviceTemplateGroups']['globalIdentifier'] = utils.query_filter(guid) kwargs['filter'] = _filter.to_dict() @@ -81,6 +80,7 @@ def list_public_images(self, guid=None, name=None, limit=100, **kwargs): kwargs['mask'] = IMAGE_MASK _filter = utils.NestedDict(kwargs.get('filter') or {}) + _filter['id'] = utils.query_filter_orderby() if name: _filter['name'] = utils.query_filter(name) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 49af7197f..261f6f91b 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -496,6 +496,7 @@ def list_subnets(self, identifier=None, datacenter=None, version=0, kwargs['mask'] = DEFAULT_SUBNET_MASK _filter = utils.NestedDict(kwargs.get('filter') or {}) + _filter['subnets']['id'] = utils.query_filter_orderby() if identifier: _filter['subnets']['networkIdentifier'] = ( diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 9159eaaa6..4e1cb8636 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -6,6 +6,7 @@ """ import collections +import copy import datetime from json import JSONDecoder import re @@ -41,6 +42,32 @@ def lookup(dic, key, *keys): return dic.get(key) +def has_key_value(d: dict, key: str = "operation", value: str = "orderBy") -> bool: + """Scan through a dictionary looking for an orderBy clause, but can be used for any key/value combo""" + if d.get(key) and d.get(key) == value: + return True + for x in d.values(): + if isinstance(x, dict): + if has_key_value(x, key, value): + return True + return False + + +def fix_filter(sl_filter: dict = None) -> dict: + """Forces an object filter to have an orderBy clause if it doesn't have one already""" + + if sl_filter is None: + sl_filter = {} + + # Make a copy to prevent sl_filter from being modified by this function + this_filter = copy.copy(sl_filter) + if not has_key_value(this_filter, "operation", "orderBy"): + # Check to see if 'id' is already a filter, if so just skip + if not this_filter.get('id', False): + this_filter['id'] = query_filter_orderby() + return this_filter + + class NestedDict(dict): """This helps with accessing a heavily nested dictionary. diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py index d21c8dece..a64efc432 100644 --- a/tests/CLI/modules/block_tests.py +++ b/tests/CLI/modules/block_tests.py @@ -125,7 +125,8 @@ def test_volume_detail_name_identifier(self): 'storageType': { 'keyName': {'operation': '*= BLOCK_STORAGE'} }, - 'username': {'operation': '_= SL-12345'} + 'username': {'operation': '_= SL-12345'}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } diff --git a/tests/CLI/modules/event_log_tests.py b/tests/CLI/modules/event_log_tests.py index 0f6bfa789..c7afacd8b 100644 --- a/tests/CLI/modules/event_log_tests.py +++ b/tests/CLI/modules/event_log_tests.py @@ -31,10 +31,9 @@ def test_get_event_log_empty(self): mock.return_value = None result = self.run_command(['event-log', 'get']) - expected = 'Event, Object, Type, Date, Username\n' \ - 'No logs available for filter {}.\n' + self.assert_no_fail(result) - self.assertEqual(expected, result.output) + self.assertIn("No logs available for filter ", result.output) def test_get_event_log_over_limit(self): result = self.run_command(['event-log', 'get', '-l 1']) diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py index c68ac7a08..616afdd5c 100644 --- a/tests/CLI/modules/file_tests.py +++ b/tests/CLI/modules/file_tests.py @@ -210,14 +210,13 @@ def test_volume_detail_name_identifier(self): expected_filter = { 'nasNetworkStorage': { 'serviceResource': { - 'type': { - 'type': {'operation': '!~ NAS'} - } + 'type': {'type': {'operation': '!~ NAS'}} }, - 'storageType': { - 'keyName': {'operation': '*= FILE_STORAGE'} - }, - 'username': {'operation': '_= SL-12345'}}} + 'storageType': {'keyName': {'operation': '*= FILE_STORAGE'}}, + 'username': {'operation': '_= SL-12345'}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } + } self.assert_called_with('SoftLayer_Account', 'getNasNetworkStorage', filter=expected_filter) self.assert_called_with('SoftLayer_Network_Storage', 'getObject', identifier=1) diff --git a/tests/api_tests.py b/tests/api_tests.py index 438d77020..0ba0a51ad 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -169,8 +169,8 @@ def test_iter_call(self, _call): self.assertEqual(list(range(125)), result) _call.assert_has_calls([ - mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0), - mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100), + mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0, filter=mock.ANY), + mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100, filter=mock.ANY), ]) _call.reset_mock() @@ -183,9 +183,9 @@ def test_iter_call(self, _call): result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True)) self.assertEqual(list(range(200)), result) _call.assert_has_calls([ - mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0), - mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100), - mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=200), + mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0, filter=mock.ANY), + mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100, filter=mock.ANY), + mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=200, filter=mock.ANY), ]) _call.reset_mock() @@ -194,12 +194,11 @@ def test_iter_call(self, _call): transports.SoftLayerListResult(range(0, 25), 30), transports.SoftLayerListResult(range(25, 30), 30) ] - result = list(self.client.iter_call( - 'SERVICE', 'METHOD', iter=True, limit=25)) + result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True, limit=25)) self.assertEqual(list(range(30)), result) _call.assert_has_calls([ - mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=0), - mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=25), + mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=0, filter=mock.ANY), + mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=25, filter=mock.ANY), ]) _call.reset_mock() @@ -208,7 +207,7 @@ def test_iter_call(self, _call): result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True)) self.assertEqual(["test"], result) _call.assert_has_calls([ - mock.call('SERVICE', 'METHOD', iter=False, limit=100, offset=0), + mock.call('SERVICE', 'METHOD', iter=False, limit=100, offset=0, filter=mock.ANY), ]) _call.reset_mock() @@ -216,23 +215,19 @@ def test_iter_call(self, _call): transports.SoftLayerListResult(range(0, 25), 30), transports.SoftLayerListResult(range(25, 30), 30) ] - result = list(self.client.iter_call('SERVICE', 'METHOD', 'ARG', - iter=True, - limit=25, - offset=12)) + result = list( + self.client.iter_call('SERVICE', 'METHOD', 'ARG', iter=True, limit=25, offset=12) + ) self.assertEqual(list(range(30)), result) _call.assert_has_calls([ - mock.call('SERVICE', 'METHOD', 'ARG', - iter=False, limit=25, offset=12), - mock.call('SERVICE', 'METHOD', 'ARG', - iter=False, limit=25, offset=37), + mock.call('SERVICE', 'METHOD', 'ARG', iter=False, limit=25, offset=12, filter=mock.ANY), + mock.call('SERVICE', 'METHOD', 'ARG', iter=False, limit=25, offset=37, filter=mock.ANY), ]) # Chunk size of 0 is invalid self.assertRaises( AttributeError, - lambda: list(self.client.iter_call('SERVICE', 'METHOD', - iter=True, limit=0))) + lambda: list(self.client.iter_call('SERVICE', 'METHOD', iter=True, limit=0, filter=mock.ANY))) def test_call_invalid_arguments(self): self.assertRaises( diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py index 54dfc6e36..2b0954c7d 100644 --- a/tests/managers/block_tests.py +++ b/tests/managers/block_tests.py @@ -125,8 +125,7 @@ def test_get_block_volume_details(self): def test_list_block_volumes(self): result = self.block.list_block_volumes() - self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, - result) + self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, result) expected_filter = { 'iscsiNetworkStorage': { @@ -134,10 +133,9 @@ def test_list_block_volumes(self): 'keyName': {'operation': '*= BLOCK_STORAGE'} }, 'serviceResource': { - 'type': { - 'type': {'operation': '!~ ISCSI'} - } - } + 'type': {'type': {'operation': '!~ ISCSI'}} + }, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } @@ -161,8 +159,7 @@ def test_list_block_volumes(self): def test_list_block_volumes_additional_filter_order(self): result = self.block.list_block_volumes(order=1234567) - self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, - result) + self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, result) expected_filter = { 'iscsiNetworkStorage': { @@ -170,14 +167,12 @@ def test_list_block_volumes_additional_filter_order(self): 'keyName': {'operation': '*= BLOCK_STORAGE'} }, 'serviceResource': { - 'type': { - 'type': {'operation': '!~ ISCSI'} - } + 'type': {'type': {'operation': '!~ ISCSI'}} }, 'billingItem': { - 'orderItem': { - 'order': { - 'id': {'operation': 1234567}}}} + 'orderItem': {'order': {'id': {'operation': 1234567}}} + }, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } @@ -199,27 +194,21 @@ def test_list_block_volumes_additional_filter_order(self): ) def test_list_block_volumes_with_additional_filters(self): - result = self.block.list_block_volumes(datacenter="dal09", - storage_type="Endurance", - username="username") + result = self.block.list_block_volumes(datacenter="dal09", storage_type="Endurance", username="username") - self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, - result) + self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, result) expected_filter = { 'iscsiNetworkStorage': { 'storageType': { 'keyName': {'operation': '^= ENDURANCE_BLOCK_STORAGE'} }, - 'username': {'operation': u'_= username'}, + 'username': {'operation': '_= username'}, 'serviceResource': { - 'datacenter': { - 'name': {'operation': u'_= dal09'} - }, - 'type': { - 'type': {'operation': '!~ ISCSI'} - } - } + 'datacenter': {'name': {'operation': u'_= dal09'}}, + 'type': {'type': {'operation': '!~ ISCSI'}} + }, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } diff --git a/tests/managers/dedicated_host_tests.py b/tests/managers/dedicated_host_tests.py index 3c1293726..0b46f6585 100644 --- a/tests/managers/dedicated_host_tests.py +++ b/tests/managers/dedicated_host_tests.py @@ -714,7 +714,8 @@ def test_list_guests_with_filters(self): 'networkComponents': {'maxSpeed': {'operation': 100}}, 'primaryIpAddress': {'operation': '_= 1.2.3.4'}, 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'} - } + }, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'getGuests', identifier=12345, filter=_filter) diff --git a/tests/managers/dns_tests.py b/tests/managers/dns_tests.py index 0c7c1cade..24519f6b8 100644 --- a/tests/managers/dns_tests.py +++ b/tests/managers/dns_tests.py @@ -167,13 +167,16 @@ def test_get_records(self): mock.return_value = [records[0]] self.dns_client.get_records(12345, record_type='a', host='hostname', data='a', ttl='86400') - _filter = {'resourceRecords': {'type': {'operation': '_= a'}, - 'host': {'operation': '_= hostname'}, - 'data': {'operation': '_= a'}, - 'ttl': {'operation': 86400}}} - self.assert_called_with('SoftLayer_Dns_Domain', 'getResourceRecords', - identifier=12345, - filter=_filter) + _filter = { + 'resourceRecords': { + 'type': {'operation': '_= a'}, + 'host': {'operation': '_= hostname'}, + 'data': {'operation': '_= a'}, + 'ttl': {'operation': 86400}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } + } + self.assert_called_with('SoftLayer_Dns_Domain', 'getResourceRecords', identifier=12345, filter=_filter) def test_get_record(self): record_id = 1234 diff --git a/tests/managers/event_log_tests.py b/tests/managers/event_log_tests.py index e5c220835..8ccba40a2 100644 --- a/tests/managers/event_log_tests.py +++ b/tests/managers/event_log_tests.py @@ -39,8 +39,8 @@ def test_get_event_log_types(self): def test_build_filter_no_args(self): result = self.event_log.build_filter(None, None, None, None, None, None) - - self.assertEqual(result, {}) + expected = {'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}} + self.assertDictEqual(result, expected) def test_build_filter_min_date(self): expected = { @@ -54,7 +54,8 @@ def test_build_filter_min_date(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter('10/30/2017', None, None, None, None, None) @@ -73,7 +74,8 @@ def test_build_filter_max_date(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter(None, '10/31/2017', None, None, None, None) @@ -98,7 +100,8 @@ def test_build_filter_min_max_date(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, None) @@ -117,7 +120,8 @@ def test_build_filter_min_date_pos_utc(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter('10/30/2017', None, None, None, None, '+0500') @@ -136,7 +140,8 @@ def test_build_filter_max_date_pos_utc(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter(None, '10/31/2017', None, None, None, '+0500') @@ -161,7 +166,8 @@ def test_build_filter_min_max_date_pos_utc(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, '+0500') @@ -180,7 +186,8 @@ def test_build_filter_min_date_neg_utc(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter('10/30/2017', None, None, None, None, '-0300') @@ -199,7 +206,8 @@ def test_build_filter_max_date_neg_utc(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter(None, '10/31/2017', None, None, None, '-0300') @@ -224,7 +232,8 @@ def test_build_filter_min_max_date_neg_utc(self): ] } ] - } + }, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, '-0300') @@ -232,21 +241,30 @@ def test_build_filter_min_max_date_neg_utc(self): self.assertEqual(expected, result) def test_build_filter_name(self): - expected = {'eventName': {'operation': 'Add Security Group'}} + expected = { + 'eventName': {'operation': 'Add Security Group'}, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } result = self.event_log.build_filter(None, None, 'Add Security Group', None, None, None) self.assertEqual(expected, result) def test_build_filter_id(self): - expected = {'objectId': {'operation': 1}} + expected = { + 'objectId': {'operation': 1}, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } result = self.event_log.build_filter(None, None, None, 1, None, None) self.assertEqual(expected, result) def test_build_filter_type(self): - expected = {'objectName': {'operation': 'CCI'}} + expected = { + 'objectName': {'operation': 'CCI'}, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } result = self.event_log.build_filter(None, None, None, None, 'CCI', None) diff --git a/tests/managers/file_tests.py b/tests/managers/file_tests.py index 11e35c001..647ddfc2c 100644 --- a/tests/managers/file_tests.py +++ b/tests/managers/file_tests.py @@ -356,7 +356,8 @@ def test_list_file_volumes(self): 'type': { 'type': {'operation': '!~ NAS'} } - } + }, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } @@ -389,14 +390,12 @@ def test_list_file_volumes_additional_filter_order(self): 'keyName': {'operation': '*= FILE_STORAGE'} }, 'serviceResource': { - 'type': { - 'type': {'operation': '!~ NAS'} - } + 'type': {'type': {'operation': '!~ NAS'}} }, 'billingItem': { - 'orderItem': { - 'order': { - 'id': {'operation': 1234567}}}} + 'orderItem': {'order': {'id': {'operation': 1234567}}} + }, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } @@ -430,15 +429,12 @@ def test_list_file_volumes_with_additional_filters(self): 'storageType': { 'keyName': {'operation': '^= ENDURANCE_FILE_STORAGE'} }, - 'username': {'operation': u'_= username'}, + 'username': {'operation': '_= username'}, 'serviceResource': { - 'datacenter': { - 'name': {'operation': u'_= dal09'} - }, - 'type': { - 'type': {'operation': '!~ NAS'} - } - } + 'datacenter': {'name': {'operation': '_= dal09'}}, + 'type': {'type': {'operation': '!~ NAS'}} + }, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } diff --git a/tests/managers/image_tests.py b/tests/managers/image_tests.py index 13c5b0fdf..82a17d6ad 100644 --- a/tests/managers/image_tests.py +++ b/tests/managers/image_tests.py @@ -46,7 +46,9 @@ def test_list_private_images_with_filters(self): 'privateBlockDeviceTemplateGroups': { 'globalIdentifier': { 'operation': '_= 0FA9ECBD-CF7E-4A1F-1E36F8D27C2B'}, - 'name': {'operation': '_= name'}} + 'name': {'operation': '_= name'}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } } self.assertEqual(len(results), 2) self.assert_called_with('SoftLayer_Account', 'getPrivateBlockDeviceTemplateGroups', filter=_filter) @@ -64,7 +66,8 @@ def test_list_public_images_with_filters(self): _filter = { 'globalIdentifier': { 'operation': '_= 0FA9ECBD-CF7E-4A1F-1E36F8D27C2B'}, - 'name': {'operation': '_= name'} + 'name': {'operation': '_= name'}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } self.assert_called_with(IMAGE_SERVICE, 'getPublicImages', filter=_filter) diff --git a/tests/managers/network_tests.py b/tests/managers/network_tests.py index aa86fe245..915edad4d 100644 --- a/tests/managers/network_tests.py +++ b/tests/managers/network_tests.py @@ -327,6 +327,7 @@ def test_list_subnets_default(self): 'version': {'operation': 4}, 'subnetType': {'operation': '_= PRIMARY'}, 'networkVlan': {'networkSpace': {'operation': '_= PUBLIC'}}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} } } @@ -602,7 +603,10 @@ def test_get_security_group_event_logs(self): result = self.network._get_security_group_event_logs() # Event log now returns a generator, so you have to get a result for it to make an API call log = result.__next__() - _filter = {'objectName': {'operation': 'Security Group'}} + _filter = { + 'objectName': {'operation': 'Security Group'}, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=_filter) self.assertEqual(100, log['accountId']) @@ -611,7 +615,10 @@ def test_get_cci_event_logs(self): result = self.network._get_cci_event_logs() # Event log now returns a generator, so you have to get a result for it to make an API call log = result.__next__() - _filter = {'objectName': {'operation': 'CCI'}} + _filter = { + 'objectName': {'operation': 'CCI'}, + 'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=_filter) self.assertEqual(100, log['accountId']) diff --git a/tests/managers/vs/vs_capacity_tests.py b/tests/managers/vs/vs_capacity_tests.py index 6fa6599e8..fbebc55a5 100644 --- a/tests/managers/vs/vs_capacity_tests.py +++ b/tests/managers/vs/vs_capacity_tests.py @@ -49,8 +49,13 @@ def test_get_available_routers(self): def test_get_available_routers_search(self): result = self.manager.get_available_routers('wdc07') - package_filter = {'keyName': {'operation': 'RESERVED_CAPACITY'}} - pod_filter = {'datacenterName': {'operation': 'wdc07'}} + package_filter = { + 'keyName': {'operation': 'RESERVED_CAPACITY'} + } + pod_filter = { + 'datacenterName': {'operation': 'wdc07'}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects', mask=mock.ANY, filter=package_filter) self.assert_called_with('SoftLayer_Product_Package', 'getRegions', mask=mock.ANY) self.assert_called_with('SoftLayer_Network_Pod', 'getAllObjects', filter=pod_filter) diff --git a/tests/utils_tests.py b/tests/utils_tests.py new file mode 100644 index 000000000..a1e1bc1f8 --- /dev/null +++ b/tests/utils_tests.py @@ -0,0 +1,88 @@ +""" + SoftLayer.tests.utils_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Tests shared code + + :license: MIT, see LICENSE for more details. +""" +from SoftLayer import testing +from SoftLayer import utils + + +TEST_FILTER = { + 'virtualGuests': { + 'provisionDate': { + 'operation': 'orderBy', + 'options': [ + {'name': 'sort', 'value': ['DESC']}, + {'name': 'sortOrder', 'value': [1]} + ] + }, + 'maxMemory': { + 'operation': 'orderBy', + 'options': [ + {'name': 'sort', 'value': ['ASC']}, + {'name': 'sortOrder', 'value': [0]} + ] + }, + }, + 'hardware': { + 'sparePoolBillingItem': { + 'id': {'operation': 'not null'} + } + }, + 'someProperty': { + 'provisionDate': { + 'operation': '> sysdate - 30' + } + } +} + + +class TestUtils(testing.TestCase): + + def test_find_key_simple(self): + """Simple test case""" + test_dict = {"key1": "value1", "nested": {"key2": "value2", "key3": "value4"}} + result = utils.has_key_value(test_dict, "key2", "value2") + print(result) + self.assertIsNotNone(result) + self.assertTrue(result) + + def test_find_object_filter(self): + """Find first orderBy operation in a real-ish object filter""" + + result = utils.has_key_value(TEST_FILTER) + print(result) + self.assertIsNotNone(result) + self.assertTrue(result) + + def test_not_found(self): + """Nothing to be found""" + test_dict = {"key1": "value1", "nested": {"key2": "value2", "key3": "value4"}} + result = utils.has_key_value(test_dict, "key23", "value2") + print(result) + self.assertFalse(result) + + def test_fix_filter(self): + original_filter = {} + fixed_filter = utils.fix_filter(original_filter) + self.assertIsNotNone(fixed_filter) + self.assertEqual(fixed_filter.get('id'), utils.query_filter_orderby()) + # testing to make sure original doesn't get changed by the function call + self.assertIsNone(original_filter.get('id')) + + def test_billing_filter(self): + billing_filter = { + 'allTopLevelBillingItems': { + 'cancellationDate': {'operation': 'is null'}, + 'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]} + } + } + + fixed_filter = utils.fix_filter(billing_filter) + print(fixed_filter) + # Make sure we didn't add any more items + self.assertEqual(len(fixed_filter), 1) + self.assertEqual(len(fixed_filter.get('allTopLevelBillingItems')), 2) + self.assertDictEqual(fixed_filter, billing_filter) From 2044aba00a2b3304c93218391a974392ff66f461 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 13 Dec 2024 15:53:07 -0600 Subject: [PATCH 54/77] Removed some debug code --- tests/utils_tests.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/utils_tests.py b/tests/utils_tests.py index a1e1bc1f8..2ee8474ca 100644 --- a/tests/utils_tests.py +++ b/tests/utils_tests.py @@ -45,7 +45,6 @@ def test_find_key_simple(self): """Simple test case""" test_dict = {"key1": "value1", "nested": {"key2": "value2", "key3": "value4"}} result = utils.has_key_value(test_dict, "key2", "value2") - print(result) self.assertIsNotNone(result) self.assertTrue(result) @@ -53,7 +52,6 @@ def test_find_object_filter(self): """Find first orderBy operation in a real-ish object filter""" result = utils.has_key_value(TEST_FILTER) - print(result) self.assertIsNotNone(result) self.assertTrue(result) @@ -61,7 +59,6 @@ def test_not_found(self): """Nothing to be found""" test_dict = {"key1": "value1", "nested": {"key2": "value2", "key3": "value4"}} result = utils.has_key_value(test_dict, "key23", "value2") - print(result) self.assertFalse(result) def test_fix_filter(self): @@ -81,7 +78,6 @@ def test_billing_filter(self): } fixed_filter = utils.fix_filter(billing_filter) - print(fixed_filter) # Make sure we didn't add any more items self.assertEqual(len(fixed_filter), 1) self.assertEqual(len(fixed_filter.get('allTopLevelBillingItems')), 2) From e7de42b7429a58312e693f56aa78b4c950839a67 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 16 Dec 2024 18:52:03 -0600 Subject: [PATCH 55/77] 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 56/77] 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): From 8bc1a9cf93d500f7fb52432ffc5d3b33c5671838 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 9 Jan 2025 13:14:18 -0600 Subject: [PATCH 57/77] Updated vlan masks to fix id/number fields not being retrieved properly. Fixed #2204 --- SoftLayer/CLI/vlan/detail.py | 56 ++++++++++++++++++++++------------- SoftLayer/CLI/vlan/list.py | 36 ++++++++++++---------- SoftLayer/managers/network.py | 23 ++++++-------- 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/SoftLayer/CLI/vlan/detail.py b/SoftLayer/CLI/vlan/detail.py index 6e16d9d8c..8c772d8bf 100644 --- a/SoftLayer/CLI/vlan/detail.py +++ b/SoftLayer/CLI/vlan/detail.py @@ -12,14 +12,11 @@ @click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('identifier') -@click.option('--no-vs', - is_flag=True, +@click.option('--no-vs', is_flag=True, help="Hide virtual server listing") -@click.option('--no-hardware', - is_flag=True, +@click.option('--no-hardware', is_flag=True, help="Hide hardware listing") -@click.option('--no-trunks', - is_flag=True, +@click.option('--no-trunks', is_flag=True, help="Hide devices with trunks") @environment.pass_env def cli(env, identifier, no_vs, no_hardware, no_trunks): @@ -28,11 +25,24 @@ def cli(env, identifier, no_vs, no_hardware, no_trunks): vlan_id = helpers.resolve_id(mgr.resolve_vlan_ids, identifier, 'VLAN') - mask = """mask[firewallInterfaces,primaryRouter[id, fullyQualifiedDomainName, datacenter], - totalPrimaryIpAddressCount,networkSpace,billingItem,hardware,subnets,virtualGuests, - networkVlanFirewall[id,fullyQualifiedDomainName,primaryIpAddress],attachedNetworkGateway[id,name,networkFirewall], - networkComponentTrunks[networkComponent[downlinkComponent[networkComponentGroup[membersDescription], - hardware[tagReferences]]]]]""" + mask = """mask[ +firewallInterfaces, primaryRouter[id, fullyQualifiedDomainName, datacenter[longName]], +totalPrimaryIpAddressCount, +networkSpace, id, vlanNumber, fullyQualifiedName, name, +hardware[id, hostname, domain, primaryIpAddress, primaryBackendIpAddress, tagReferences], +subnets[id, networkIdentifier, netmask, gateway, subnetType, usableIpAddressCount], +virtualGuests[id, hostname, domain, primaryIpAddress, primaryBackendIpAddress], +networkVlanFirewall[id,fullyQualifiedDomainName,primaryIpAddress], +attachedNetworkGateway[id,name,networkFirewall], +networkComponentTrunks[ + networkComponent[ + downlinkComponent[ + networkComponentGroup[membersDescription], + hardware[tagReferences] + ] + ] +] +]""" vlan = mgr.get_vlan(vlan_id, mask=mask) @@ -42,10 +52,8 @@ def cli(env, identifier, no_vs, no_hardware, no_trunks): table.add_row(['id', vlan.get('id')]) table.add_row(['number', vlan.get('vlanNumber')]) - table.add_row(['datacenter', - utils.lookup(vlan, 'primaryRouter', 'datacenter', 'longName')]) - table.add_row(['primary_router', - utils.lookup(vlan, 'primaryRouter', 'fullyQualifiedDomainName')]) + table.add_row(['datacenter', utils.lookup(vlan, 'primaryRouter', 'datacenter', 'longName')]) + table.add_row(['primary_router', utils.lookup(vlan, 'primaryRouter', 'fullyQualifiedDomainName')]) table.add_row(['Gateway/Firewall', get_gateway_firewall(vlan)]) if vlan.get('subnets'): @@ -93,12 +101,7 @@ def cli(env, identifier, no_vs, no_hardware, no_trunks): trunks = filter_trunks(vlan.get('networkComponentTrunks')) trunks_table = formatting.Table(['device', 'port', 'tags']) for trunk in trunks: - trunks_table.add_row([utils.lookup(trunk, 'networkComponent', 'downlinkComponent', - 'hardware', 'fullyQualifiedDomainName'), - utils.lookup(trunk, 'networkComponent', 'downlinkComponent', - 'networkComponentGroup', 'membersDescription'), - formatting.tags(utils.lookup(trunk, 'networkComponent', 'downlinkComponent', - 'hardware', 'tagReferences'))]) + trunks_table.add_row(get_trunk_row(trunk)) table.add_row(['trunks', trunks_table]) else: table.add_row(['trunks', '-']) @@ -106,6 +109,17 @@ def cli(env, identifier, no_vs, no_hardware, no_trunks): env.fout(table) +def get_trunk_row(trunk: dict) -> list: + """Parses a vlan trunk and returns a table row for it""" + dl_component = utils.lookup(trunk, 'networkComponent', 'downlinkComponent') + row = [ + utils.lookup(dl_component, 'hardware', 'fullyQualifiedDomainName'), + utils.lookup(dl_component, 'networkComponentGroup', 'membersDescription'), + formatting.tags(utils.lookup(dl_component, 'hardware', 'tagReferences')) + ] + return row + + def get_gateway_firewall(vlan): """Gets the name of a gateway/firewall from a VLAN. """ diff --git a/SoftLayer/CLI/vlan/list.py b/SoftLayer/CLI/vlan/list.py index ed55561db..918d58988 100644 --- a/SoftLayer/CLI/vlan/list.py +++ b/SoftLayer/CLI/vlan/list.py @@ -9,19 +9,21 @@ from SoftLayer.CLI.vlan.detail import get_gateway_firewall from SoftLayer import utils -COLUMNS = ['Id', - 'Number', - 'Fully qualified name', - 'Name', - 'Network', - 'Data center', - 'Pod', - 'Gateway/Firewall', - 'Hardware', - 'Virtual servers', - 'Public ips', - 'Premium', - 'Tags'] +COLUMNS = [ + 'Id', + 'Number', + 'Fully qualified name', + 'Name', + 'Network', + 'Data center', + 'Pod', + 'Gateway/Firewall', + 'Hardware', + 'Virtual servers', + 'Public ips', + 'Premium', + 'Tags' +] @click.command(cls=SoftLayer.CLI.command.SLCommand, ) @@ -49,9 +51,11 @@ def cli(env, sortby, datacenter, number, name, limit): name=name, limit=limit) - mask = """mask[name, datacenterLongName, frontendRouterId, capabilities, datacenterId, backendRouterId, - backendRouterName, frontendRouterName]""" - pods = mgr.get_pods(mask=mask) + pod_mask = """mask[ +name, datacenterLongName, capabilities, datacenterId, +backendRouterId, backendRouterName, frontendRouterName, frontendRouterId +]""" + pods = mgr.get_pods(mask=pod_mask) for vlan in vlans: billing = 'Yes' if vlan.get('billingItem') else 'No' diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 261f6f91b..6ac46a741 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -38,19 +38,7 @@ 'addressSpace', 'endPointIpAddress' ]) -DEFAULT_VLAN_MASK = ','.join([ - 'firewallInterfaces', - 'hardwareCount', - 'primaryRouter[id, fullyQualifiedDomainName, datacenter]', - 'subnetCount', - 'billingItem', - 'totalPrimaryIpAddressCount', - 'virtualGuestCount', - 'networkSpace', - 'networkVlanFirewall[id,fullyQualifiedDomainName,primaryIpAddress]', - 'attachedNetworkGateway[id,name,networkFirewall]', - 'tagReferences[tag[name]]', -]) + DEFAULT_GET_VLAN_MASK = ','.join([ 'firewallInterfaces', 'primaryRouter[id, fullyQualifiedDomainName, datacenter]', @@ -528,6 +516,13 @@ def list_vlans(self, datacenter=None, vlan_number=None, name=None, limit=100, ma :param dict \\*\\*kwargs: response-level options (mask, limit, etc.) """ + vlan_mask = """mask[ +networkSpace, id, vlanNumber, fullyQualifiedName, name, +networkVlanFirewall[id,fullyQualifiedDomainName], attachedNetworkGateway[id,name], +firewallInterfaces, primaryRouter[id, fullyQualifiedDomainName, datacenter[name]], +hardwareCount, subnetCount, totalPrimaryIpAddressCount, virtualGuestCount, +billingItem[id], tagReferences[tag[name]] +]""" _filter = utils.NestedDict(_filter or {}) _filter['networkVlans']['id'] = utils.query_filter_orderby() @@ -542,7 +537,7 @@ def list_vlans(self, datacenter=None, vlan_number=None, name=None, limit=100, ma _filter['networkVlans']['primaryRouter']['datacenter']['name'] = utils.query_filter(datacenter) if mask is None: - mask = DEFAULT_VLAN_MASK + mask = vlan_mask # cf_call uses threads to get all results. return self.client.cf_call('SoftLayer_Account', 'getNetworkVlans', From a272ef6f40de8f356c5b0f843ebe86d153730c94 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 9 Jan 2025 13:30:34 -0600 Subject: [PATCH 58/77] v6.2.6 version bump --- SoftLayer/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 136e2800d..2a1d23eaf 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v6.2.5' +VERSION = 'v6.2.6' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' From 1f5c7a05fed24a99bcd5871595c694e66fc73b84 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 9 Jan 2025 13:52:56 -0600 Subject: [PATCH 59/77] Fixed v6.2.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e539fb5e2..6e02544b2 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='v6.2.5', + version='v6.2.6', description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type='text/x-rst', From c80fe7f733ad22a5685ef04d430fbf5d53f77522 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 14 Jan 2025 16:12:00 -0600 Subject: [PATCH 60/77] Migrated away from using Vlan.primaryRouter to find a VLAN's datacenter --- SoftLayer/CLI/subnet/detail.py | 38 ++++++++++++++++++---------------- SoftLayer/CLI/vlan/detail.py | 4 ++-- SoftLayer/CLI/vlan/list.py | 15 ++++---------- SoftLayer/managers/network.py | 7 +++---- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/SoftLayer/CLI/subnet/detail.py b/SoftLayer/CLI/subnet/detail.py index 4fd82ba5d..b62cc1525 100644 --- a/SoftLayer/CLI/subnet/detail.py +++ b/SoftLayer/CLI/subnet/detail.py @@ -26,9 +26,17 @@ def cli(env, identifier, no_vs, no_hardware): subnet_id = helpers.resolve_id(mgr.resolve_subnet_ids, identifier, name='subnet') - mask = 'mask[ipAddresses[id, ipAddress, note, isBroadcast, isGateway, isNetwork, isReserved, ' \ - 'hardware, virtualGuest], datacenter, virtualGuests, hardware,' \ - ' networkVlan[networkSpace,primaryRouter]]' + mask = """mask[ +networkIdentifier, cidr, subnetType, gateway, broadcastAddress, usableIpAddressCount, note, id, +ipAddresses[ + id, ipAddress, note, isBroadcast, isGateway, isNetwork, isReserved, + hardware[id, fullyQualifiedDomainName], + virtualGuest[id, fullyQualifiedDomainName] +], +datacenter[name], networkVlan[networkSpace], tagReferences, +virtualGuests[id, fullyQualifiedDomainName, hostname, domain, primaryIpAddress, primaryBackendIpAddress], +hardware[id, fullyQualifiedDomainName, hostname, domain, primaryIpAddress, primaryBackendIpAddress] +]""" subnet = mgr.get_subnet(subnet_id, mask=mask) @@ -37,22 +45,15 @@ def cli(env, identifier, no_vs, no_hardware): table.align['value'] = 'l' table.add_row(['id', subnet['id']]) - table.add_row(['identifier', - '%s/%s' % (subnet['networkIdentifier'], - str(subnet['cidr']))]) + table.add_row(['identifier', f"{subnet['networkIdentifier']}/{subnet['cidr']}"]) table.add_row(['subnet type', subnet.get('subnetType', formatting.blank())]) - table.add_row(['network space', - utils.lookup(subnet, 'networkVlan', 'networkSpace')]) + table.add_row(['network space', utils.lookup(subnet, 'networkVlan', 'networkSpace')]) table.add_row(['gateway', subnet.get('gateway', formatting.blank())]) - table.add_row(['broadcast', - subnet.get('broadcastAddress', formatting.blank())]) + table.add_row(['broadcast', subnet.get('broadcastAddress', formatting.blank())]) table.add_row(['datacenter', subnet['datacenter']['name']]) - table.add_row(['usable ips', - subnet.get('usableIpAddressCount', formatting.blank())]) - table.add_row(['note', - subnet.get('note', formatting.blank())]) - table.add_row(['tags', - formatting.tags(subnet.get('tagReferences'))]) + table.add_row(['usable ips', subnet.get('usableIpAddressCount', formatting.blank())]) + table.add_row(['note', subnet.get('note', formatting.blank())]) + table.add_row(['tags', formatting.tags(subnet.get('tagReferences'))]) ip_address = subnet.get('ipAddresses') @@ -72,8 +73,9 @@ def cli(env, identifier, no_vs, no_hardware): elif address.get('virtualGuest') is not None: description = address['virtualGuest']['fullyQualifiedDomainName'] status = 'In use' - ip_table.add_row([address.get('id'), status, - address.get('ipAddress') + '/' + description, address.get('note', '-')]) + ip_table.add_row([ + address.get('id'), status, f"{address.get('ipAddress')}/{description}", address.get('note', '-') + ]) table.add_row(['ipAddresses', ip_table]) diff --git a/SoftLayer/CLI/vlan/detail.py b/SoftLayer/CLI/vlan/detail.py index 8c772d8bf..250461813 100644 --- a/SoftLayer/CLI/vlan/detail.py +++ b/SoftLayer/CLI/vlan/detail.py @@ -26,7 +26,7 @@ def cli(env, identifier, no_vs, no_hardware, no_trunks): vlan_id = helpers.resolve_id(mgr.resolve_vlan_ids, identifier, 'VLAN') mask = """mask[ -firewallInterfaces, primaryRouter[id, fullyQualifiedDomainName, datacenter[longName]], +firewallInterfaces, datacenter[name, longName], primaryRouter[fullyQualifiedDomainName], totalPrimaryIpAddressCount, networkSpace, id, vlanNumber, fullyQualifiedName, name, hardware[id, hostname, domain, primaryIpAddress, primaryBackendIpAddress, tagReferences], @@ -52,7 +52,7 @@ def cli(env, identifier, no_vs, no_hardware, no_trunks): table.add_row(['id', vlan.get('id')]) table.add_row(['number', vlan.get('vlanNumber')]) - table.add_row(['datacenter', utils.lookup(vlan, 'primaryRouter', 'datacenter', 'longName')]) + table.add_row(['datacenter', utils.lookup(vlan, 'datacenter', 'longName')]) table.add_row(['primary_router', utils.lookup(vlan, 'primaryRouter', 'fullyQualifiedDomainName')]) table.add_row(['Gateway/Firewall', get_gateway_firewall(vlan)]) diff --git a/SoftLayer/CLI/vlan/list.py b/SoftLayer/CLI/vlan/list.py index 918d58988..9a8df890b 100644 --- a/SoftLayer/CLI/vlan/list.py +++ b/SoftLayer/CLI/vlan/list.py @@ -46,15 +46,9 @@ def cli(env, sortby, datacenter, number, name, limit): table = formatting.Table(COLUMNS) table.sortby = sortby - vlans = mgr.list_vlans(datacenter=datacenter, - vlan_number=number, - name=name, - limit=limit) + vlans = mgr.list_vlans(datacenter=datacenter, vlan_number=number, name=name, limit=limit) - pod_mask = """mask[ -name, datacenterLongName, capabilities, datacenterId, -backendRouterId, backendRouterName, frontendRouterName, frontendRouterId -]""" + pod_mask = """mask[name, capabilities]""" pods = mgr.get_pods(mask=pod_mask) for vlan in vlans: @@ -66,7 +60,7 @@ def cli(env, sortby, datacenter, number, name, limit): vlan.get('fullyQualifiedName'), vlan.get('name') or formatting.blank(), vlan.get('networkSpace', 'Direct Link').capitalize(), - utils.lookup(vlan, 'primaryRouter', 'datacenter', 'name'), + utils.lookup(vlan, 'datacenter', 'name'), get_pod_with_closed_announcement(vlan, pods), get_gateway_firewall(vlan), vlan.get('hardwareCount'), @@ -82,8 +76,7 @@ def cli(env, sortby, datacenter, number, name, limit): def get_pod_with_closed_announcement(vlan, pods): """Gets pods with announcement to close""" for pod in pods: - if utils.lookup(pod, 'backendRouterId') == utils.lookup(vlan, 'primaryRouter', 'id') \ - or utils.lookup(pod, 'frontendRouterId') == utils.lookup(vlan, 'primaryRouter', 'id'): + if utils.lookup(pod, 'name') == utils.lookup(vlan, 'podName'): if 'CLOSURE_ANNOUNCED' in utils.lookup(pod, 'capabilities'): name_pod = utils.lookup(pod, 'name').split('.')[1] + '*' return "[red]" + name_pod.capitalize() + "[/red]" diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 6ac46a741..fa8172b42 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -517,11 +517,10 @@ def list_vlans(self, datacenter=None, vlan_number=None, name=None, limit=100, ma """ vlan_mask = """mask[ -networkSpace, id, vlanNumber, fullyQualifiedName, name, +networkSpace, id, vlanNumber, fullyQualifiedName, name, datacenter[name], podName, networkVlanFirewall[id,fullyQualifiedDomainName], attachedNetworkGateway[id,name], -firewallInterfaces, primaryRouter[id, fullyQualifiedDomainName, datacenter[name]], -hardwareCount, subnetCount, totalPrimaryIpAddressCount, virtualGuestCount, -billingItem[id], tagReferences[tag[name]] +firewallInterfaces, billingItem[id], tagReferences[tag[name]], +hardwareCount, subnetCount, totalPrimaryIpAddressCount, virtualGuestCount ]""" _filter = utils.NestedDict(_filter or {}) From 7fa769a0dfeeeced4fb0b9bd9e59a4637f7c45d2 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 14 Feb 2025 13:46:35 -0600 Subject: [PATCH 61/77] #2211 removed CDN commands and created a deprecated message for them --- SoftLayer/CLI/cdn/cdn.py | 16 ++ SoftLayer/CLI/cdn/create.py | 56 ----- SoftLayer/CLI/cdn/delete.py | 21 -- SoftLayer/CLI/cdn/detail.py | 46 ---- SoftLayer/CLI/cdn/edit.py | 93 -------- SoftLayer/CLI/cdn/list.py | 44 ---- SoftLayer/CLI/cdn/origin_add.py | 106 --------- SoftLayer/CLI/cdn/origin_list.py | 28 --- SoftLayer/CLI/cdn/origin_remove.py | 20 -- SoftLayer/CLI/cdn/purge.py | 40 ---- SoftLayer/CLI/command.py | 1 + SoftLayer/CLI/routes.py | 18 +- SoftLayer/managers/cdn.py | 355 ----------------------------- SoftLayer/utils.py | 2 + tests/CLI/modules/cdn_tests.py | 186 --------------- 15 files changed, 28 insertions(+), 1004 deletions(-) create mode 100644 SoftLayer/CLI/cdn/cdn.py delete mode 100644 SoftLayer/CLI/cdn/create.py delete mode 100644 SoftLayer/CLI/cdn/delete.py delete mode 100644 SoftLayer/CLI/cdn/detail.py delete mode 100644 SoftLayer/CLI/cdn/edit.py delete mode 100644 SoftLayer/CLI/cdn/list.py delete mode 100644 SoftLayer/CLI/cdn/origin_add.py delete mode 100644 SoftLayer/CLI/cdn/origin_list.py delete mode 100644 SoftLayer/CLI/cdn/origin_remove.py delete mode 100644 SoftLayer/CLI/cdn/purge.py delete mode 100644 SoftLayer/managers/cdn.py delete mode 100644 tests/CLI/modules/cdn_tests.py diff --git a/SoftLayer/CLI/cdn/cdn.py b/SoftLayer/CLI/cdn/cdn.py new file mode 100644 index 000000000..3fb38c62e --- /dev/null +++ b/SoftLayer/CLI/cdn/cdn.py @@ -0,0 +1,16 @@ +"""https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, deprecated=True) +@environment.pass_env +def cli(env): + """https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation""" + pass \ No newline at end of file diff --git a/SoftLayer/CLI/cdn/create.py b/SoftLayer/CLI/cdn/create.py deleted file mode 100644 index c23d91e51..000000000 --- a/SoftLayer/CLI/cdn/create.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Create a CDN domain mapping.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.option('--hostname', required=True, help="To route requests to your website, enter the hostname for your" - "website, for example, www.example.com or app.example.com.") -@click.option('--origin', required=True, help="Your server IP address or hostname.") -@click.option('--origin-type', default="server", type=click.Choice(['server', 'storage']), show_default=True, - help="The origin type. Note: If OriginType is storage then OriginHost is take as Endpoint") -@click.option('--http', help="Http port") -@click.option('--https', help="Https port") -@click.option('--bucket-name', help="Bucket name") -@click.option('--cname', help="Enter a globally unique subdomain. The full URL becomes the CNAME we use to configure" - " your DNS. If no value is entered, we will generate a CNAME for you.") -@click.option('--header', help="The edge server uses the host header in the HTTP header to communicate with the" - " Origin host. It defaults to Hostname.") -@click.option('--path', help="Give a path relative to the domain provided, which can be used to reach this Origin." - " For example, 'articles/video' => 'www.example.com/articles/video") -@click.option('--ssl', default="dvSan", type=click.Choice(['dvSan', 'wilcard']), help="A DV SAN Certificate allows" - " HTTPS traffic over your personal domain, but it requires a domain validation to prove ownership." - " A wildcard certificate allows HTTPS traffic only when using the CNAME given.") -@environment.pass_env -def cli(env, hostname, origin, origin_type, http, https, bucket_name, cname, header, path, ssl): - """Create a CDN domain mapping.""" - if not http and not https: - raise exceptions.CLIAbort('Is needed http or https options') - - manager = SoftLayer.CDNManager(env.client) - cdn = manager.create_cdn(hostname, origin, origin_type, http, https, bucket_name, cname, header, path, ssl) - - table = formatting.Table(['Name', 'Value']) - table.add_row(['CDN Unique ID', cdn.get('uniqueId')]) - if bucket_name: - table.add_row(['Bucket Name', cdn.get('bucketName')]) - table.add_row(['Hostname', cdn.get('domain')]) - table.add_row(['Header', cdn.get('header')]) - table.add_row(['IBM CNAME', cdn.get('cname')]) - table.add_row(['Akamai CNAME', cdn.get('akamaiCname')]) - table.add_row(['Origin Host', cdn.get('originHost')]) - table.add_row(['Origin Type', cdn.get('originType')]) - table.add_row(['Protocol', cdn.get('protocol')]) - table.add_row(['Http Port', cdn.get('httpPort')]) - table.add_row(['Https Port', cdn.get('httpsPort')]) - table.add_row(['Certificate Type', cdn.get('certificateType')]) - table.add_row(['Provider', cdn.get('vendorName')]) - table.add_row(['Path', cdn.get('path')]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/delete.py b/SoftLayer/CLI/cdn/delete.py deleted file mode 100644 index 0dd2e91d6..000000000 --- a/SoftLayer/CLI/cdn/delete.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Delete a CDN domain mapping.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('unique_id') -@environment.pass_env -def cli(env, unique_id): - """Delete a CDN domain mapping.""" - - manager = SoftLayer.CDNManager(env.client) - - cdn = manager.delete_cdn(unique_id) - - if cdn: - env.fout(f"Cdn with uniqueId: {unique_id} was deleted.") diff --git a/SoftLayer/CLI/cdn/detail.py b/SoftLayer/CLI/cdn/detail.py deleted file mode 100644 index 973b1acc5..000000000 --- a/SoftLayer/CLI/cdn/detail.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Detail a CDN Account.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('unique_id') -@click.option('--history', - default=30, type=click.IntRange(1, 89), - help='Bandwidth, Hits, Ratio counted over history number of days ago. 89 is the maximum. ') -@environment.pass_env -def cli(env, unique_id, history): - """Detail a CDN Account.""" - - manager = SoftLayer.CDNManager(env.client) - - cdn_mapping = manager.get_cdn(unique_id) - cdn_metrics = manager.get_usage_metrics(unique_id, history=history) - - # usage metrics - total_bandwidth = "%s GB" % cdn_metrics['totals'][0] - total_hits = cdn_metrics['totals'][1] - hit_ratio = "%s %%" % cdn_metrics['totals'][2] - - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - - table.add_row(['unique_id', cdn_mapping['uniqueId']]) - table.add_row(['hostname', cdn_mapping['domain']]) - table.add_row(['protocol', cdn_mapping['protocol']]) - table.add_row(['origin', cdn_mapping['originHost']]) - table.add_row(['origin_type', cdn_mapping['originType']]) - table.add_row(['path', cdn_mapping['path']]) - table.add_row(['provider', cdn_mapping['vendorName']]) - table.add_row(['status', cdn_mapping['status']]) - table.add_row(['total_bandwidth', total_bandwidth]) - table.add_row(['total_hits', total_hits]) - table.add_row(['hit_ratio', hit_ratio]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/edit.py b/SoftLayer/CLI/cdn/edit.py deleted file mode 100644 index df4f17947..000000000 --- a/SoftLayer/CLI/cdn/edit.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Edit a CDN Account.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer.CLI import helpers - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('identifier') -@click.option('--header', '-H', - type=click.STRING, - help="Host header." - ) -@click.option('--http-port', '-t', - type=click.INT, - help="HTTP port." - ) -@click.option('--https-port', '-s', - type=click.INT, - help="HTTPS port." - ) -@click.option('--origin', '-o', - type=click.STRING, - help="Origin server address." - ) -@click.option('--respect-headers', '-r', - type=click.Choice(['1', '0']), - help="Respect headers. The value 1 is On and 0 is Off." - ) -@click.option('--cache', '-c', type=str, - help="Cache key optimization. These are the valid options to choose: 'include-all', 'ignore-all', " - "'include-specified', 'ignore-specified'. If you select 'include-specified' or 'ignore-specified' " - "please add to option --cache-description.\n" - " e.g --cache=include-specified --cache-description=description." - ) -@click.option('--cache-description', '-C', type=str, - help="In cache option, if you select 'include-specified' or 'ignore-specified', " - "please add a description too using this option.\n" - "e.g --cache include-specified --cache-description description." - ) -@click.option('--performance-configuration', '-p', - type=click.Choice(['General web delivery', 'Large file optimization', 'Video on demand optimization']), - help="Optimize for, General web delivery', 'Large file optimization', 'Video on demand optimization', " - "the Dynamic content acceleration option is not added because this has a special configuration." - ) -@environment.pass_env -def cli(env, identifier, header, http_port, https_port, origin, respect_headers, cache, - cache_description, performance_configuration): - """Edit a CDN Account. - - Note: You can use the hostname or uniqueId as IDENTIFIER. - """ - - manager = SoftLayer.CDNManager(env.client) - cdn_id = helpers.resolve_id(manager.resolve_ids, identifier, 'CDN') - - cache_result = {} - if cache or cache_description: - if len(cache) > 1: - cache_result['cacheKeyQueryRule'] = cache - else: - cache_result['cacheKeyQueryRule'] = cache[0] - - cdn_result = manager.edit(cdn_id, header=header, http_port=http_port, https_port=https_port, origin=origin, - respect_headers=respect_headers, cache=cache_result, cache_description=cache_description, - performance_configuration=performance_configuration) - - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - - for cdn in cdn_result: - table.add_row(['Create Date', cdn.get('createDate')]) - table.add_row(['Header', cdn.get('header')]) - if cdn.get('httpPort'): - table.add_row(['Http Port', cdn.get('httpPort')]) - if cdn.get('httpsPort'): - table.add_row(['Https Port', cdn.get('httpsPort')]) - table.add_row(['Origin Type', cdn.get('originType')]) - table.add_row(['Performance Configuration', cdn.get('performanceConfiguration')]) - table.add_row(['Protocol', cdn.get('protocol')]) - table.add_row(['Respect Headers', cdn.get('respectHeaders')]) - table.add_row(['Unique Id', cdn.get('uniqueId')]) - table.add_row(['Vendor Name', cdn.get('vendorName')]) - table.add_row(['Cache key optimization', cdn.get('cacheKeyQueryRule')]) - table.add_row(['cname', cdn.get('cname')]) - table.add_row(['Origin server address', cdn.get('originHost')]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/list.py b/SoftLayer/CLI/cdn/list.py deleted file mode 100644 index fb269994f..000000000 --- a/SoftLayer/CLI/cdn/list.py +++ /dev/null @@ -1,44 +0,0 @@ -"""List CDN Accounts.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.option('--sortby', - help='Column to sort by', - type=click.Choice(['unique_id', - 'domain', - 'origin', - 'vendor', - 'cname', - 'status'])) -@environment.pass_env -def cli(env, sortby): - """List all CDN accounts.""" - - manager = SoftLayer.CDNManager(env.client) - accounts = manager.list_cdn() - - table = formatting.Table(['unique_id', - 'domain', - 'origin', - 'vendor', - 'cname', - 'status']) - for account in accounts: - table.add_row([ - account['uniqueId'], - account['domain'], - account['originHost'], - account['vendorName'], - account['cname'], - account['status'] - ]) - - table.sortby = sortby - env.fout(table) diff --git a/SoftLayer/CLI/cdn/origin_add.py b/SoftLayer/CLI/cdn/origin_add.py deleted file mode 100644 index 7a77b0260..000000000 --- a/SoftLayer/CLI/cdn/origin_add.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Create an origin pull mapping.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('unique_id') -@click.argument('origin') -@click.argument('path') -@click.option('--origin-type', '-t', - type=click.Choice(['server', 'storage']), - help='The origin type.', - default='server', - show_default=True) -@click.option('--header', '-H', - type=click.STRING, - help='The host header to communicate with the origin.') -@click.option('--bucket-name', '-b', - type=click.STRING, - help="The name of the available resource [required if --origin-type=storage]") -@click.option('--http-port', '-p', - type=click.INT, - help="The http port number. [http or https is required]") -@click.option('--https-port', '-s', - type=click.INT, - help="The https port number. [http or https is required]" - ) -@click.option('--protocol', '-P', - type=click.STRING, - help="The protocol used by the origin.", - default='http', - show_default=True) -@click.option('--optimize-for', '-o', - type=click.Choice(['web', 'video', 'file', 'dynamic']), - help="Performance configuration", - default='web', - show_default=True) -@click.option('--dynamic-path', '-d', - help="The path that Akamai edge servers periodically fetch the test object from." - "example = /detection-test-object.html") -@click.option('--compression', '-i', - help="Enable or disable compression of JPEG images for requests over certain network conditions.", - default='true', - show_default=True) -@click.option('--prefetching', '-g', - help="Enable or disable the embedded object prefetching feature.", - default='true', - show_default=True) -@click.option('--extensions', '-e', - type=click.STRING, - help="File extensions that can be stored in the CDN, example: 'jpg, png, pdf'") -@click.option('--cache-query', '-c', - type=click.STRING, - help="Cache query rules with the following formats:\n" - "'ignore-all', 'include: ', 'ignore: '", - default="include-all", - show_default=True) -@environment.pass_env -def cli(env, unique_id, origin, path, origin_type, header, - bucket_name, http_port, https_port, protocol, optimize_for, - dynamic_path, compression, prefetching, - extensions, cache_query): - """Create an origin path for an existing CDN mapping. - - For more information see the following documentation: \n - https://cloud.ibm.com/docs/infrastructure/CDN?topic=CDN-manage-your-cdn#adding-origin-path-details - """ - - manager = SoftLayer.CDNManager(env.client) - - if origin_type == 'storage' and not bucket_name: - raise exceptions.ArgumentError('[-b | --bucket-name] is required when [-t | --origin-type] is "storage"') - - result = manager.add_origin(unique_id, origin, path, dynamic_path, origin_type=origin_type, - header=header, http_port=http_port, https_port=https_port, protocol=protocol, - bucket_name=bucket_name, file_extensions=extensions, - optimize_for=optimize_for, - compression=compression, prefetching=prefetching, - cache_query=cache_query) - - table = formatting.Table(['Item', 'Value']) - table.align['Item'] = 'r' - table.align['Value'] = 'r' - - table.add_row(['CDN Unique ID', result['mappingUniqueId']]) - - if origin_type == 'storage': - table.add_row(['Bucket Name', result['bucketName']]) - - table.add_row(['Origin', result['origin']]) - table.add_row(['Origin Type', result['originType']]) - table.add_row(['Header', result['header']]) - table.add_row(['Path', result['path']]) - table.add_row(['Http Port', result['httpPort']]) - table.add_row(['Https Port', result['httpsPort']]) - table.add_row(['Cache Key Rule', result['cacheKeyQueryRule']]) - table.add_row(['Configuration', result['performanceConfiguration']]) - table.add_row(['Status', result['status']]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/origin_list.py b/SoftLayer/CLI/cdn/origin_list.py deleted file mode 100644 index f2dc03082..000000000 --- a/SoftLayer/CLI/cdn/origin_list.py +++ /dev/null @@ -1,28 +0,0 @@ -"""List origin pull mappings.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('unique_id') -@environment.pass_env -def cli(env, unique_id): - """List origin path for an existing CDN mapping.""" - - manager = SoftLayer.CDNManager(env.client) - origins = manager.get_origins(unique_id) - - table = formatting.Table(['Path', 'Origin', 'HTTP Port', 'Status']) - - for origin in origins: - table.add_row([origin['path'], - origin['origin'], - origin['httpPort'], - origin['status']]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/origin_remove.py b/SoftLayer/CLI/cdn/origin_remove.py deleted file mode 100644 index a7767b419..000000000 --- a/SoftLayer/CLI/cdn/origin_remove.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Remove an origin pull mapping.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('unique_id') -@click.argument('origin_path') -@environment.pass_env -def cli(env, unique_id, origin_path): - """Removes an origin path for an existing CDN mapping.""" - - manager = SoftLayer.CDNManager(env.client) - manager.remove_origin(unique_id, origin_path) - - click.secho("Origin with path %s has been deleted" % origin_path, fg='green') diff --git a/SoftLayer/CLI/cdn/purge.py b/SoftLayer/CLI/cdn/purge.py deleted file mode 100644 index 97bf88319..000000000 --- a/SoftLayer/CLI/cdn/purge.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Purge cached files from all edge nodes.""" -# :license: MIT, see LICENSE for more details. -import datetime - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('unique_id') -@click.argument('path') -@environment.pass_env -def cli(env, unique_id, path): - """Creates a purge record and also initiates the purge call. - - Example: - slcli cdn purge 9779455 /article/file.txt - - For more information see the following documentation: \n - https://cloud.ibm.com/docs/infrastructure/CDN?topic=CDN-manage-your-cdn#purging-cached-content - """ - - manager = SoftLayer.CDNManager(env.client) - result = manager.purge_content(unique_id, path) - - table = formatting.Table(['Date', 'Path', 'Saved', 'Status']) - - for data in result: - date = datetime.datetime.fromtimestamp(int(data['date'])) - table.add_row([ - date, - data['path'], - data['saved'], - data['status'] - ]) - - env.fout(table) diff --git a/SoftLayer/CLI/command.py b/SoftLayer/CLI/command.py index 70f5adbc9..b4cbd768c 100644 --- a/SoftLayer/CLI/command.py +++ b/SoftLayer/CLI/command.py @@ -31,6 +31,7 @@ class OptionHighlighter(RegexHighlighter): r"(?PExample::)", r"(?P(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)" r"(?P^[A-Z]+$)", + r"(?P\(Deprecated\) .*$)" ] diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 705b2cac6..c2d517e2d 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -75,15 +75,15 @@ ('dedicatedhost:list-guests', 'SoftLayer.CLI.dedicatedhost.list_guests:cli'), ('cdn', 'SoftLayer.CLI.cdn'), - ('cdn:detail', 'SoftLayer.CLI.cdn.detail:cli'), - ('cdn:edit', 'SoftLayer.CLI.cdn.edit:cli'), - ('cdn:list', 'SoftLayer.CLI.cdn.list:cli'), - ('cdn:origin-add', 'SoftLayer.CLI.cdn.origin_add:cli'), - ('cdn:origin-list', 'SoftLayer.CLI.cdn.origin_list:cli'), - ('cdn:origin-remove', 'SoftLayer.CLI.cdn.origin_remove:cli'), - ('cdn:purge', 'SoftLayer.CLI.cdn.purge:cli'), - ('cdn:delete', 'SoftLayer.CLI.cdn.delete:cli'), - ('cdn:create', 'SoftLayer.CLI.cdn.create:cli'), + ('cdn:detail', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:edit', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:list', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:origin-add', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:origin-list', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:origin-remove', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:purge', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:delete', 'SoftLayer.CLI.cdn.cdn:cli'), + ('cdn:create', 'SoftLayer.CLI.cdn.cdn:cli'), ('config', 'SoftLayer.CLI.config'), ('config:setup', 'SoftLayer.CLI.config.setup:cli'), diff --git a/SoftLayer/managers/cdn.py b/SoftLayer/managers/cdn.py deleted file mode 100644 index 0f3a26f02..000000000 --- a/SoftLayer/managers/cdn.py +++ /dev/null @@ -1,355 +0,0 @@ -""" - SoftLayer.cdn - ~~~~~~~~~~~~~ - CDN Manager/helpers - - :license: MIT, see LICENSE for more details. -""" -import SoftLayer -from SoftLayer import utils - - -# pylint: disable=too-many-lines,too-many-instance-attributes - - -class CDNManager(utils.IdentifierMixin, object): - """Manage Content Delivery Networks in the account. - - See product information here: - https://www.ibm.com/cloud/cdn - https://cloud.ibm.com/docs/infrastructure/CDN?topic=CDN-about-content-delivery-networks-cdn- - - :param SoftLayer.API.BaseClient client: the client instance - """ - - def __init__(self, client): - self.client = client - self._start_date = None - self._end_date = None - self.cdn_configuration = self.client['Network_CdnMarketplace_Configuration_Mapping'] - self.cdn_path = self.client['SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path'] - self.cdn_metrics = self.client['Network_CdnMarketplace_Metrics'] - self.cdn_purge = self.client['SoftLayer_Network_CdnMarketplace_Configuration_Cache_Purge'] - self.resolvers = [self._get_ids_from_hostname] - - def list_cdn(self, **kwargs): - """Lists Content Delivery Networks for the active user. - - :param dict \\*\\*kwargs: header-level options (mask, limit, etc.) - :returns: The list of CDN objects in the account - """ - - return self.cdn_configuration.listDomainMappings(**kwargs) - - def get_cdn(self, unique_id, **kwargs): - """Retrieves the information about the CDN account object. - - :param str unique_id: The unique ID associated with the CDN. - :param dict \\*\\*kwargs: header-level option (mask) - :returns: The CDN object - """ - - cdn_list = self.cdn_configuration.listDomainMappingByUniqueId(unique_id, **kwargs) - - # The method listDomainMappingByUniqueId() returns an array but there is only 1 object - return cdn_list[0] - - def get_origins(self, unique_id, **kwargs): - """Retrieves list of origin pull mappings for a specified CDN account. - - :param str unique_id: The unique ID associated with the CDN. - :param dict \\*\\*kwargs: header-level options (mask, limit, etc.) - :returns: The list of origin paths in the CDN object. - """ - - return self.cdn_path.listOriginPath(unique_id, **kwargs) - - def add_origin(self, unique_id, origin, path, dynamic_path, origin_type="server", header=None, - http_port=80, https_port=None, protocol='http', bucket_name=None, file_extensions=None, - optimize_for="web", compression=None, prefetching=None, - cache_query="include all"): - """Creates an origin path for an existing CDN. - - :param str unique_id: The unique ID associated with the CDN. - :param str path: relative path to the domain provided, e.g. "/articles/video" - :param str dynamic_path: The path that Akamai edge servers periodically fetch the test object from. - example = /detection-test-object.html - :param str origin: ip address or hostname if origin_type=server, API endpoint for - your S3 object storage if origin_type=storage - :param str origin_type: it can be 'server' or 'storage' types. - :param str header: the edge server uses the host header to communicate with the origin. - It defaults to hostname. (optional) - :param int http_port: the http port number (default: 80) - :param int https_port: the https port number - :param str protocol: the protocol of the origin (default: HTTP) - :param str bucket_name: name of the available resource - :param str file_extensions: file extensions that can be stored in the CDN, e.g. "jpg,png" - :param str optimize_for: performance configuration, available options: web, video, and file where: - - - 'web' = 'General web delivery' - - 'video' = 'Video on demand optimization' - - 'file' = 'Large file optimization' - - 'dynamic' = 'Dynamic content acceleration' - :param bool compression: Enable or disable compression of JPEG images for requests over - certain network conditions. - :param bool prefetching: Enable or disable the embedded object prefetching feature. - :param str cache_query: rules with the following formats: 'include-all', 'ignore-all', - 'include: space separated query-names', - 'ignore: space separated query-names'.' - :return: a CDN origin path object - """ - types = {'server': 'HOST_SERVER', 'storage': 'OBJECT_STORAGE'} - performance_config = { - 'web': 'General web delivery', - 'video': 'Video on demand optimization', - 'file': 'Large file optimization', - "dynamic": "Dynamic content acceleration" - } - - new_origin = { - 'uniqueId': unique_id, - 'path': path, - 'origin': origin, - 'originType': types.get(origin_type), - 'httpPort': http_port, - 'httpsPort': https_port, - 'protocol': protocol.upper(), - 'performanceConfiguration': performance_config.get(optimize_for), - 'cacheKeyQueryRule': cache_query, - } - - if optimize_for == 'dynamic': - new_origin['dynamicContentAcceleration'] = { - 'detectionPath': "/" + str(dynamic_path), - 'prefetchEnabled': bool(prefetching), - 'mobileImageCompressionEnabled': bool(compression) - } - - if header: - new_origin['header'] = header - - if types.get(origin_type) == 'OBJECT_STORAGE': - if bucket_name: - new_origin['bucketName'] = bucket_name - - if file_extensions: - new_origin['fileExtension'] = file_extensions - - origin = self.cdn_path.createOriginPath(new_origin) - - # The method createOriginPath() returns an array but there is only 1 object - return origin[0] - - def remove_origin(self, unique_id, path): - """Removes an origin pull mapping with the given origin pull ID. - - :param str unique_id: The unique ID associated with the CDN. - :param str path: The origin path to delete. - :returns: A string value - """ - - return self.cdn_path.deleteOriginPath(unique_id, path) - - def purge_content(self, unique_id, path): - """Purges a URL or path from the CDN. - - :param str unique_id: The unique ID associated with the CDN. - :param str path: A string of url or path that should be purged. - :returns: A Container_Network_CdnMarketplace_Configuration_Cache_Purge array object - """ - return self.cdn_purge.createPurge(unique_id, path) - - def get_usage_metrics(self, unique_id, history=30, frequency="aggregate"): - """Retrieves the cdn usage metrics. - - It uses the 'days' argument if start_date and end_date are None. - - :param int unique_id: The CDN uniqueId from which the usage metrics will be obtained. - :param int history: Last N days, default days is 30. - :param str frequency: It can be day, week, month and aggregate. The default is "aggregate". - :returns: A Container_Network_CdnMarketplace_Metrics object - """ - - _start = utils.days_to_datetime(history) - _end = utils.days_to_datetime(0) - - self._start_date = utils.timestamp(_start) - self._end_date = utils.timestamp(_end) - - usage = self.cdn_metrics.getMappingUsageMetrics(unique_id, self._start_date, self._end_date, frequency) - - # The method getMappingUsageMetrics() returns an array but there is only 1 object - return usage[0] - - @property - def start_data(self): - """Retrieve the cdn usage metric start date.""" - return self._start_date - - @property - def end_date(self): - """Retrieve the cdn usage metric end date.""" - return self._end_date - - def edit(self, identifier, header=None, http_port=None, https_port=None, origin=None, - respect_headers=None, cache=None, cache_description=None, performance_configuration=None): - """Edit the cdn object. - - :param string identifier: The CDN identifier. - :param header: The cdn Host header. - :param http_port: The cdn HTTP port. - :param https_port: The cdn HTTPS port. - :param origin: The cdn Origin server address. - :param respect_headers: The cdn Respect headers. - :param cache: The cdn Cache key optimization. - :param performance_configuration: The cdn performance configuration. - - :returns: SoftLayer_Container_Network_CdnMarketplace_Configuration_Mapping[]. - """ - cdn_instance_detail = self.get_cdn(str(identifier)) - - config = { - 'uniqueId': cdn_instance_detail.get('uniqueId'), - 'originType': cdn_instance_detail.get('originType'), - 'protocol': cdn_instance_detail.get('protocol'), - 'path': cdn_instance_detail.get('path'), - 'vendorName': cdn_instance_detail.get('vendorName'), - 'cname': cdn_instance_detail.get('cname'), - 'domain': cdn_instance_detail.get('domain'), - 'origin': cdn_instance_detail.get('originHost'), - 'header': cdn_instance_detail.get('header') - } - if cdn_instance_detail.get('httpPort'): - config['httpPort'] = cdn_instance_detail.get('httpPort') - - if cdn_instance_detail.get('httpsPort'): - config['httpsPort'] = cdn_instance_detail.get('httpsPort') - - if header: - config['header'] = header - - if http_port: - config['httpPort'] = http_port - - if https_port: - config['httpsPort'] = https_port - - if origin: - config['origin'] = origin - - if respect_headers: - config['respectHeaders'] = respect_headers - - if cache or cache_description: - if 'include-specified' in cache['cacheKeyQueryRule']: - cache_key_rule = self.get_cache_key_query_rule('include', cache_description) - config['cacheKeyQueryRule'] = cache_key_rule - elif 'ignore-specified' in cache['cacheKeyQueryRule']: - cache_key_rule = self.get_cache_key_query_rule('ignore', cache_description) - config['cacheKeyQueryRule'] = cache_key_rule - else: - config['cacheKeyQueryRule'] = cache['cacheKeyQueryRule'] - - if performance_configuration: - config['performanceConfiguration'] = performance_configuration - - return self.cdn_configuration.updateDomainMapping(config) - - def _get_ids_from_hostname(self, hostname): - """Get the cdn object detail. - - :param string hostname: The CDN identifier. - :returns: SoftLayer_Container_Network_CdnMarketplace_Configuration_Mapping[]. - """ - result = [] - cdn_list = self.cdn_configuration.listDomainMappings() - for cdn in cdn_list: - if cdn.get('domain', '').lower() == hostname.lower(): - result.append(cdn.get('uniqueId')) - break - - return result - - @staticmethod - def get_cache_key_query_rule(cache_type, cache_description): - """Get the cdn object detail. - - :param string cache_type: Cache type. - :param cache: Cache description. - - :return: string value. - """ - if cache_description is None: - raise SoftLayer.SoftLayerError('Please add a description to be able to update the' - ' cache.') - cache_result = '%s: %s' % (cache_type, cache_description) - - return cache_result - - def delete_cdn(self, unique_id): - """Delete CDN domain mapping for a particular customer. - - :param str unique_id: The unique ID associated with the CDN. - :returns: The cdn that is being deleted. - """ - - return self.cdn_configuration.deleteDomainMapping(unique_id) - - def create_cdn(self, hostname=None, origin=None, origin_type=None, http=None, https=None, bucket_name=None, - cname=None, header=None, path=None, ssl=None): - """Create CDN domain mapping for a particular customer. - - :param str hostname: The unique ID associated with the CDN. - :param str origin: ip address or hostname if origin_type=server, API endpoint for - your S3 object storage if origin_type=storage - :param str origin_type: it can be 'server' or 'storage' types. - :param int http: http port - :param int https: https port - :param str bucket_name: name of the available resource - :param str cname: globally unique subdomain - :param str header: the edge server uses the host header to communicate with the origin. - It defaults to hostname. (optional) - :param str path: relative path to the domain provided, e.g. "/articles/video" - :param str ssl: ssl certificate - :returns: The cdn that is being created. - """ - types = {'server': 'HOST_SERVER', 'storage': 'OBJECT_STORAGE'} - ssl_certificate = {'wilcard': 'WILDCARD_CERT', 'dvSan': 'SHARED_SAN_CERT'} - - new_origin = { - 'domain': hostname, - 'origin': origin, - 'originType': types.get(origin_type), - 'vendorName': 'akamai', - } - - protocol = '' - if http: - protocol = 'HTTP' - new_origin['httpPort'] = http - if https: - protocol = 'HTTPS' - new_origin['httpsPort'] = https - new_origin['certificateType'] = ssl_certificate.get(ssl) - if http and https: - protocol = 'HTTP_AND_HTTPS' - - new_origin['protocol'] = protocol - - if types.get(origin_type) == 'OBJECT_STORAGE': - new_origin['bucketName'] = bucket_name - new_origin['header'] = header - - if cname: - new_origin['cname'] = cname + '.cdn.appdomain.cloud' - - if header: - new_origin['header'] = header - - if path: - new_origin['path'] = '/' + path - - origin = self.cdn_configuration.createDomainMapping(new_origin) - - # The method createOriginPath() returns an array but there is only 1 object - return origin[0] diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 4e1cb8636..5258234dd 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -521,6 +521,7 @@ def console_color_themes(theme): "option_choices": "gold3", "example_block": "underline deep_pink3", "url": "underline blue", + "deprecated": "underline red", }) ) return Console(theme=Theme( @@ -540,6 +541,7 @@ def console_color_themes(theme): "option_choices": "gold3", "example_block": "underline light_coral", "url": "underline blue", + "deprecated": "underline red", }) ) diff --git a/tests/CLI/modules/cdn_tests.py b/tests/CLI/modules/cdn_tests.py deleted file mode 100644 index f10bafe7b..000000000 --- a/tests/CLI/modules/cdn_tests.py +++ /dev/null @@ -1,186 +0,0 @@ -""" - SoftLayer.tests.CLI.modules.cdn_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - :license: MIT, see LICENSE for more details. -""" -import datetime -import json -from unittest import mock as mock - -from SoftLayer.CLI import exceptions -from SoftLayer import testing - - -class CdnTests(testing.TestCase): - - def test_list_accounts(self): - result = self.run_command(['cdn', 'list']) - - self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - [{'cname': 'cdnakauuiet7s6u6.cdnedge.bluemix.net', - 'domain': 'test.example.com', - 'origin': '1.1.1.1', - 'status': 'CNAME_CONFIGURATION', - 'unique_id': '11223344', - 'vendor': 'akamai'}] - ) - - @mock.patch('SoftLayer.utils.days_to_datetime') - def test_detail_account(self, mock_now): - mock_now.return_value = datetime.datetime(2020, 1, 1) - result = self.run_command(['cdn', 'detail', '--history=30', '1245']) - - self.assert_no_fail(result) - api_results = json.loads(result.output) - self.assertEqual(api_results['hit_ratio'], '2.0 %') - self.assertEqual(api_results['total_bandwidth'], '1.0 GB') - self.assertEqual(api_results['total_hits'], 3) - self.assertEqual(api_results['hostname'], 'test.example.com') - self.assertEqual(api_results['protocol'], 'HTTP') - - def test_purge_content(self): - result = self.run_command(['cdn', 'purge', '1234', - '/article/file.txt']) - - self.assert_no_fail(result) - - def test_list_origins(self): - result = self.run_command(['cdn', 'origin-list', '1234']) - - self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), [{'HTTP Port': 80, - 'Origin': '10.10.10.1', - 'Path': '/example', - 'Status': 'RUNNING'}, - {'HTTP Port': 80, - 'Origin': '10.10.10.1', - 'Path': '/example1', - 'Status': 'RUNNING'}]) - - def test_add_origin_server(self): - result = self.run_command( - ['cdn', 'origin-add', '-t', 'server', '-H=test.example.com', '-p', 80, '-o', 'web', '-c=include-all', - '1234', '10.10.10.1', '/example/videos2']) - - self.assert_no_fail(result) - - def test_add_origin_server_dynamic(self): - result = self.run_command( - ['cdn', 'origin-add', '-t', 'server', '-H=test.example.com', '-s', 81, '-o', 'dynamic', '-c=include-all', - '-P', 'HTTPS', '-d', 'abc.html', '-g', True, '-i', True, '1234', '10.10.10.1', '/example/videos2', ]) - - self.assert_no_fail(result) - - def test_add_origin_storage(self): - result = self.run_command(['cdn', 'origin-add', '-t', 'storage', '-b=test-bucket', '-H=test.example.com', - '-p', 80, '-o', 'web', '-c=include-all', '1234', '10.10.10.1', '/example/videos2']) - - self.assert_no_fail(result) - - def test_add_origin_storage_dynamic(self): - result = self.run_command(['cdn', 'origin-add', '-t', 'storage', '-b=test-bucket', '-H=test.example.com', - '-s', 81, '-o', 'dynamic', '-c=include-all', '1234', '10.10.10.1', - '/example/videos2', '-g', True, '-i', True]) - - self.assert_no_fail(result) - - def test_add_origin_without_storage(self): - result = self.run_command(['cdn', 'origin-add', '-t', 'storage', '-H=test.example.com', '-p', 80, - '-P', 'HTTPS', '-o', 'web', '-c=include-all', - '1234', '10.10.10.1', '/example/videos2']) - - self.assertEqual(result.exit_code, 2) - self.assertIsInstance(result.exception, exceptions.ArgumentError) - - def test_add_origin_storage_with_file_extensions(self): - result = self.run_command( - ['cdn', 'origin-add', '-t', 'storage', '-b=test-bucket', '-e', 'jpg', '-H=test.example.com', '-p', 80, - '-o', 'web', '-c=include-all', '1234', '10.10.10.1', '/example/videos2']) - - self.assert_no_fail(result) - - def test_add_origin_storage_with_file_extensions_dynamic(self): - result = self.run_command( - ['cdn', 'origin-add', '-t', 'storage', '-b=test-bucket', '-e', 'jpg', '-H=test.example.com', '-s', 81, - '-P', 'HTTPS', '-o', 'dynamic', '-d', 'abc.html', '-g', True, '-i', True, - '-c=include-all', '1234', '10.10.10.1', '/example/videos2', - ]) - - self.assert_no_fail(result) - - def test_remove_origin(self): - result = self.run_command(['cdn', 'origin-remove', '1234', - '/example1']) - - self.assert_no_fail(result) - self.assertEqual(result.output, "Origin with path /example1 has been deleted\n") - - def test_edit_header(self): - result = self.run_command(['cdn', 'edit', 'test.example.com', '--header=www.test.com']) - self.assert_no_fail(result) - header_result = json.loads(result.output) - self.assertEqual('www.test.com', header_result['Header']) - - def test_edit_http_port(self): - result = self.run_command(['cdn', 'edit', 'test.example.com', '--http-port=83']) - self.assert_no_fail(result) - header_result = json.loads(result.output) - self.assertEqual(83, header_result['Http Port']) - - def test_edit_respect_headers(self): - result = self.run_command(['cdn', 'edit', 'test.example.com', '--respect-headers=1']) - self.assert_no_fail(result) - header_result = json.loads(result.output) - self.assertEqual(True, header_result['Respect Headers']) - - def test_edit_cache(self): - result = self.run_command(['cdn', 'edit', 'test.example.com', '--cache', 'include-specified', - '--cache', 'test']) - self.assert_no_fail(result) - header_result = json.loads(result.output) - self.assertEqual('include: test', header_result['Cache key optimization']) - - def test_edit_cache_by_uniqueId(self): - result = self.run_command(['cdn', 'edit', '11223344', '--cache', 'include-specified', '--cache', 'test']) - self.assert_no_fail(result) - header_result = json.loads(result.output) - self.assertEqual('include: test', header_result['Cache key optimization']) - - def test_delete_cdn(self): - result = self.run_command(['cdn', 'delete', '123456']) - self.assert_no_fail(result) - self.assertIn("Cdn with uniqueId: 123456 was deleted.", result.output) - - def test_create_cdn(self): - result = self.run_command(['cdn', 'create', '--hostname', 'www.example.com', - '--origin', '123.123.123.123', '--http', '80']) - self.assert_no_fail(result) - self.assertIn("CDN Unique ID", result.output) - self.assertIn("354034879028850", result.output) - self.assertIn("Hostname", result.output) - self.assertIn("test.com", result.output) - self.assertIn("header", result.output) - self.assertIn("header.test.com", result.output) - self.assertIn("Http Port", result.output) - self.assertIn("80", result.output) - self.assertIn("Path", result.output) - self.assertIn("/*", result.output) - - def test_create_cdn_without_hostname(self): - result = self.run_command(['cdn', 'create']) - self.assertEqual(2, result.exit_code) - print(result.output) - self.assertIn("Error: Missing option '--hostname'.", result.output) - - def test_create_cdn_without_origin(self): - result = self.run_command(['cdn', 'create', '--hostname', 'www.example.com']) - self.assertEqual(2, result.exit_code) - print(result.output) - self.assertIn("Error: Missing option '--origin'.", result.output) - - def test_create_cdn_without_http_or_https(self): - result = self.run_command(['cdn', 'create', '--hostname', 'www.example.com', '--origin', '123.123.123.123']) - self.assertEqual(2, result.exit_code) - self.assertIn("Is needed http or https options", result.exception.message) From ae74e81e0e18c75984bd73897578a03cc1fe8224 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 14 Feb 2025 14:05:36 -0600 Subject: [PATCH 62/77] #2211 fixed testing issues --- .secrets.baseline | 28 ++--- SoftLayer/CLI/cdn/cdn.py | 7 +- SoftLayer/managers/__init__.py | 2 - docs/cli/cdn.rst | 37 +++--- tests/functional_tests.py | 18 --- tests/managers/cdn_tests.py | 187 ------------------------------- tests/transports/rest_tests.py | 11 -- tests/transports/xmlrpc_tests.py | 13 --- 8 files changed, 38 insertions(+), 265 deletions(-) delete mode 100644 tests/managers/cdn_tests.py diff --git a/.secrets.baseline b/.secrets.baseline index b256d881d..394061815 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2024-10-07T21:05:06Z", + "generated_at": "2025-02-14T20:05:29Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -574,7 +574,7 @@ "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", "is_secret": false, "is_verified": false, - "line_number": 32, + "line_number": 31, "type": "Secret Keyword", "verified_result": null } @@ -584,7 +584,7 @@ "hashed_secret": "f7a9e24777ec23212c54d7a350bc5bea5477fdbb", "is_secret": false, "is_verified": false, - "line_number": 1088, + "line_number": 1077, "type": "Secret Keyword", "verified_result": null } @@ -604,7 +604,7 @@ "hashed_secret": "8de91b1f4c8ca32302ae101da16fb88fb127582a", "is_secret": false, "is_verified": false, - "line_number": 165, + "line_number": 168, "type": "Secret Keyword", "verified_result": null }, @@ -612,7 +612,7 @@ "hashed_secret": "2da422d13be8072a8dcae1e46b36add9cb2372fa", "is_secret": false, "is_verified": false, - "line_number": 190, + "line_number": 193, "type": "Secret Keyword", "verified_result": null } @@ -640,7 +640,7 @@ "hashed_secret": "2c0ceacd445f15ebc02315e18fb3ed8ec73a61a0", "is_secret": false, "is_verified": false, - "line_number": 544, + "line_number": 545, "type": "Hex High Entropy String", "verified_result": null }, @@ -648,7 +648,7 @@ "hashed_secret": "f08bf4f915242a2700e861e4e073ab45dc745e92", "is_secret": false, "is_verified": false, - "line_number": 551, + "line_number": 552, "type": "Hex High Entropy String", "verified_result": null }, @@ -656,7 +656,7 @@ "hashed_secret": "806f21b4bc195ffd5749f295b83909d66a56ff38", "is_secret": false, "is_verified": false, - "line_number": 583, + "line_number": 584, "type": "Hex High Entropy String", "verified_result": null }, @@ -664,7 +664,7 @@ "hashed_secret": "1c89f7ca3440fe5db16e3b0ffe414d11845331d9", "is_secret": false, "is_verified": false, - "line_number": 589, + "line_number": 590, "type": "Hex High Entropy String", "verified_result": null }, @@ -672,7 +672,7 @@ "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc", "is_secret": false, "is_verified": false, - "line_number": 596, + "line_number": 597, "type": "Hex High Entropy String", "verified_result": null } @@ -700,7 +700,7 @@ "hashed_secret": "8af1f8146d96a3cd862281442d0d6c5cb6f8f9e5", "is_secret": false, "is_verified": false, - "line_number": 176, + "line_number": 181, "type": "Hex High Entropy String", "verified_result": null } @@ -720,7 +720,7 @@ "hashed_secret": "9878e362285eb314cfdbaa8ee8c300c285856810", "is_secret": false, "is_verified": false, - "line_number": 324, + "line_number": 313, "type": "Secret Keyword", "verified_result": null } @@ -748,7 +748,7 @@ "hashed_secret": "f08c5dc4980df3c1237e88b872a2429dac6be328", "is_secret": false, "is_verified": false, - "line_number": 310, + "line_number": 297, "type": "Secret Keyword", "verified_result": null }, @@ -756,7 +756,7 @@ "hashed_secret": "7e6a3680012346b94b54731e13d8a9ffa3790645", "is_secret": false, "is_verified": false, - "line_number": 396, + "line_number": 383, "type": "Secret Keyword", "verified_result": null } diff --git a/SoftLayer/CLI/cdn/cdn.py b/SoftLayer/CLI/cdn/cdn.py index 3fb38c62e..7237a126a 100644 --- a/SoftLayer/CLI/cdn/cdn.py +++ b/SoftLayer/CLI/cdn/cdn.py @@ -4,13 +4,8 @@ import click import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting @click.command(cls=SoftLayer.CLI.command.SLCommand, deprecated=True) -@environment.pass_env -def cli(env): +def cli(): """https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation""" - pass \ No newline at end of file diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index 214e1ce62..8a6c8c095 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -10,7 +10,6 @@ from SoftLayer.managers.account import AccountManager from SoftLayer.managers.bandwidth import BandwidthManager from SoftLayer.managers.block import BlockStorageManager -from SoftLayer.managers.cdn import CDNManager from SoftLayer.managers.dedicated_host import DedicatedHostManager from SoftLayer.managers.dns import DNSManager from SoftLayer.managers.event_log import EventLogManager @@ -40,7 +39,6 @@ 'BandwidthManager', 'BlockStorageManager', 'CapacityManager', - 'CDNManager', 'DedicatedHostManager', 'DNSManager', 'EventLogManager', diff --git a/docs/cli/cdn.rst b/docs/cli/cdn.rst index de9e7c6f7..945e49e07 100644 --- a/docs/cli/cdn.rst +++ b/docs/cli/cdn.rst @@ -4,38 +4,47 @@ Interacting with CDN ===================== -.. click:: SoftLayer.CLI.cdn.detail:cli +.. click:: SoftLayer.CLI.cdn.cdn:cli + :prog: cdn list + :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation + +.. click:: SoftLayer.CLI.cdn.cdn:cli :prog: cdn detail :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation -.. click:: SoftLayer.CLI.cdn.list:cli - :prog: cdn list +.. click:: SoftLayer.CLI.cdn.cdn:cli + :prog: cdn edit :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation -.. click:: SoftLayer.CLI.cdn.origin_add:cli +.. click:: SoftLayer.CLI.cdn.cdn:cli :prog: cdn origin-add :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation -.. click:: SoftLayer.CLI.cdn.origin_list:cli +.. click:: SoftLayer.CLI.cdn.cdn:cli :prog: cdn origin-list :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation -.. click:: SoftLayer.CLI.cdn.origin_remove:cli +.. click:: SoftLayer.CLI.cdn.cdn:cli :prog: cdn origin-remove :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation -.. click:: SoftLayer.CLI.cdn.purge:cli +.. click:: SoftLayer.CLI.cdn.cdn:cli :prog: cdn purge :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation -.. click:: SoftLayer.CLI.cdn.edit:cli - :prog: cdn edit - :show-nested: - -.. click:: SoftLayer.CLI.cdn.delete:cli +.. click:: SoftLayer.CLI.cdn.cdn:cli :prog: cdn delete :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation -.. click:: SoftLayer.CLI.cdn.create:cli +.. click:: SoftLayer.CLI.cdn.cdn:cli :prog: cdn create - :show-nested: \ No newline at end of file + :show-nested: + DEPRECATED https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation \ No newline at end of file diff --git a/tests/functional_tests.py b/tests/functional_tests.py index dc67e29f1..c6f6b71b6 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -8,7 +8,6 @@ import SoftLayer from SoftLayer import testing -from SoftLayer import transports class FunctionalTest(testing.TestCase): @@ -34,23 +33,6 @@ def test_failed_auth(self): SoftLayer.SoftLayerAPIError, client['SoftLayer_User_Customer'].getPortalLoginToken) - def test_no_hostname(self): - try: - request = transports.Request() - request.service = 'SoftLayer_Account' - request.method = 'getObject' - request.id = 1234 - - # This test will fail if 'notvalidsoftlayer.com' becomes a thing - transport = transports.XmlRpcTransport( - endpoint_url='http://notvalidsoftlayer.com', - ) - transport(request) - except SoftLayer.TransportError as ex: - self.assertEqual(ex.faultCode, 0) - else: - self.fail('Transport Error Exception Not Raised') - class AuthedUser(FunctionalTest): def test_service_does_not_exist(self): diff --git a/tests/managers/cdn_tests.py b/tests/managers/cdn_tests.py deleted file mode 100644 index 71dcf737d..000000000 --- a/tests/managers/cdn_tests.py +++ /dev/null @@ -1,187 +0,0 @@ -""" - SoftLayer.tests.managers.cdn_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - :license: MIT, see LICENSE for more details. -""" -import datetime -from unittest import mock as mock - -from SoftLayer import fixtures -from SoftLayer.managers import cdn -from SoftLayer import testing - - -class CDNTests(testing.TestCase): - - def set_up(self): - self.cdn_client = cdn.CDNManager(self.client) - - def test_list_accounts(self): - self.cdn_client.list_cdn() - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping', - 'listDomainMappings') - - def test_detail_cdn(self): - self.cdn_client.get_cdn("12345") - - args = ("12345",) - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping', - 'listDomainMappingByUniqueId', - args=args) - - @mock.patch('SoftLayer.utils.days_to_datetime') - def test_detail_usage_metric(self, mock_now): - mock_now.return_value = datetime.datetime(2020, 1, 1) - self.cdn_client.get_usage_metrics(12345, history=30, frequency="aggregate") - - args = (12345, - self.cdn_client.start_data, - self.cdn_client.end_date, - "aggregate") - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Metrics', - 'getMappingUsageMetrics', - args=args) - - # Does this still work in 2038 ? https://github.com/softlayer/softlayer-python/issues/1764 for context - @mock.patch('SoftLayer.utils.days_to_datetime') - def test_detail_usage_metric_future(self, mock_now): - mock_now.return_value = datetime.datetime(2040, 1, 1) - self.assertRaises( - OverflowError, - self.cdn_client.get_usage_metrics, 12345, history=30, frequency="aggregate" - ) - - def test_get_origins(self): - self.cdn_client.get_origins("12345") - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', - 'listOriginPath') - - def test_add_origin(self): - self.cdn_client.add_origin("12345", "10.10.10.1", "/example/videos", dynamic_path="abc.html", - origin_type="server", header="test.example.com", https_port=81, - protocol='https', optimize_for="dynamic", compression=True, - prefetching=True, cache_query="include all") - - args = ({ - 'uniqueId': "12345", - 'origin': '10.10.10.1', - 'path': '/example/videos', - 'originType': 'HOST_SERVER', - 'header': 'test.example.com', - 'httpPort': 80, - 'httpsPort': 81, - 'protocol': 'HTTPS', - 'performanceConfiguration': 'Dynamic content acceleration', - 'cacheKeyQueryRule': "include all", - 'dynamicContentAcceleration': { - 'detectionPath': "/abc.html", - 'prefetchEnabled': True, - 'mobileImageCompressionEnabled': True - } - },) - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', - 'createOriginPath', - args=args) - - def test_add_origin_with_bucket_and_file_extension(self): - self.cdn_client.add_origin("12345", "10.10.10.1", "/example/videos", dynamic_path="abc.html", - origin_type="server", header="test.example.com", https_port=81, - protocol='https', optimize_for="dynamic", compression=True, - prefetching=True, cache_query="include all") - - args = ({ - 'uniqueId': "12345", - 'origin': '10.10.10.1', - 'path': '/example/videos', - 'originType': 'HOST_SERVER', - 'header': 'test.example.com', - 'httpPort': 80, - 'httpsPort': 81, - 'protocol': 'HTTPS', - 'performanceConfiguration': 'Dynamic content acceleration', - 'cacheKeyQueryRule': "include all", - 'dynamicContentAcceleration': { - 'detectionPath': "/abc.html", - 'prefetchEnabled': True, - 'mobileImageCompressionEnabled': True - } - },) - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', - 'createOriginPath', - args=args) - - def test_remove_origin(self): - self.cdn_client.remove_origin("12345", "/example1") - - args = ("12345", - "/example1") - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', - 'deleteOriginPath', - args=args) - - def test_purge_content(self): - self.cdn_client.purge_content("12345", "/example1") - - args = ("12345", - "/example1") - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Cache_Purge', - 'createPurge', - args=args) - - def test_cdn_edit(self): - identifier = '11223344' - header = 'www.test.com' - result = self.cdn_client.edit(identifier, header=header) - - self.assertEqual(fixtures.SoftLayer_Network_CdnMarketplace_Configuration_Mapping. - updateDomainMapping, result) - - self.assert_called_with( - 'SoftLayer_Network_CdnMarketplace_Configuration_Mapping', - 'updateDomainMapping', - args=({ - 'uniqueId': '11223344', - 'originType': 'HOST_SERVER', - 'protocol': 'HTTP', - 'path': '/', - 'vendorName': 'akamai', - 'cname': 'cdnakauuiet7s6u6.cdnedge.bluemix.net', - 'domain': 'test.example.com', - 'httpPort': 80, - 'header': 'www.test.com', - 'origin': '1.1.1.1' - },) - ) - - def test_cdn_instance_by_hostname(self): - hostname = 'test.example.com' - result = self.cdn_client._get_ids_from_hostname(hostname) - expected_result = fixtures.SoftLayer_Network_CdnMarketplace_Configuration_Mapping.listDomainMappings - self.assertEqual(expected_result[0]['uniqueId'], result[0]) - self.assert_called_with( - 'SoftLayer_Network_CdnMarketplace_Configuration_Mapping', - 'listDomainMappings',) - - def test_delete_cdn(self): - uniqueId = "123465" - self.cdn_client.delete_cdn(uniqueId) - - args = (uniqueId,) - - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping', - 'deleteDomainMapping', - args=args) - - def test_create_cdn(self): - hostname = "test.com" - origin = "123.123.123.123" - origin_type = "server" - http = 80 - newCdn = ({"domain": hostname, "origin": origin, "originType": "HOST_SERVER", - "vendorName": "akamai", "httpPort": http, "protocol": "HTTP"},) - self.cdn_client.create_cdn(hostname, origin, origin_type, http) - - self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping', - 'createDomainMapping', - args=newCdn) diff --git a/tests/transports/rest_tests.py b/tests/transports/rest_tests.py index 20186e95f..c54d28a0f 100644 --- a/tests/transports/rest_tests.py +++ b/tests/transports/rest_tests.py @@ -7,7 +7,6 @@ import json import requests from unittest import mock as mock -import warnings import SoftLayer from SoftLayer import testing @@ -100,16 +99,6 @@ def test_json_error(self, request): req.method = 'Resource' self.assertRaises(SoftLayer.SoftLayerAPIError, self.transport, req) - def test_proxy_without_protocol(self): - req = transports.Request() - req.service = 'SoftLayer_Service' - req.method = 'Resource' - req.proxy = 'localhost:3128' - try: - self.assertRaises(SoftLayer.TransportError, self.transport, req) - except AssertionError: - warnings.warn("AssertionError raised instead of a SoftLayer.TransportError error") - @mock.patch('SoftLayer.transports.rest.requests.Session.request') def test_valid_proxy(self, request): request().text = '{}' diff --git a/tests/transports/xmlrpc_tests.py b/tests/transports/xmlrpc_tests.py index 051e13822..718a51ca2 100644 --- a/tests/transports/xmlrpc_tests.py +++ b/tests/transports/xmlrpc_tests.py @@ -6,7 +6,6 @@ """ import io from unittest import mock as mock -import warnings import pytest import requests @@ -83,18 +82,6 @@ def test_call(self, request): self.assertIsInstance(resp, transports.SoftLayerListResult) self.assertEqual(resp.total_count, 10) - def test_proxy_without_protocol(self): - req = transports.Request() - req.service = 'SoftLayer_Service' - req.method = 'Resource' - req.proxy = 'localhost:3128' - - try: - self.assertRaises(SoftLayer.TransportError, self.transport, req) - except AssertionError: - warnings.warn("Incorrect Exception raised. Expected a " - "SoftLayer.TransportError error") - @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') def test_valid_proxy(self, request): request.return_value = self.response From 9728e8f24285e4f2b10f77c7326cf9c67252948e Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 20 Feb 2025 13:09:14 -0600 Subject: [PATCH 63/77] #2114 fixed an issue when placing an order doesn't find the correct datacenter --- SoftLayer/CLI/order/place.py | 2 +- tests/CLI/modules/order_tests.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/order/place.py b/SoftLayer/CLI/order/place.py index 97c41784f..eb6c45e98 100644 --- a/SoftLayer/CLI/order/place.py +++ b/SoftLayer/CLI/order/place.py @@ -65,7 +65,7 @@ def cli(env, package_keyname, location, preset, verify, billing, complex_type, pods = network.get_closed_pods() location_dc = network.get_datacenter_by_keyname(location) for pod in pods: - if location_dc.get('name') in pod.get('name'): + if location_dc and location_dc.get('name') in pod.get('name'): click.secho(f"Warning: Closed soon: {pod.get('name')}", fg='yellow') if extras: diff --git a/tests/CLI/modules/order_tests.py b/tests/CLI/modules/order_tests.py index dc2c892a6..29beca5a7 100644 --- a/tests/CLI/modules/order_tests.py +++ b/tests/CLI/modules/order_tests.py @@ -10,6 +10,7 @@ from unittest import mock as mock from SoftLayer.CLI import exceptions +from SoftLayer.exceptions import SoftLayerError as SoftLayerError from SoftLayer import testing @@ -452,6 +453,15 @@ def test_quote_delete(self): self.assert_no_fail(result) self.assert_called_with('SoftLayer_Billing_Order_Quote', 'deleteQuote', identifier='12345') + def test_empty_get_datacenter(self): + """https://github.com/softlayer/softlayer-python/issues/2114 """ + dc_mock = self.set_mock('SoftLayer_Location', 'getDatacenters') + dc_mock.side_effect = [[], [{'name': 'dal13', 'id': 123}]] + result = self.run_command(['--really', 'order', 'place', 'SOFTWARE_LICENSE_PACKAGE', 'dal13', 'SOMETHING']) + self.assertEqual(result.exit_code, 1) + self.assertIsInstance(result.exception, SoftLayerError) + self.assertEqual(str(result.exception), "A complex type must be specified with the order") + def _get_all_packages(): package_type = {'keyName': 'BARE_METAL_CPU'} From dffb607ec2f537071d90709683211496f7319ac8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:48:56 +0000 Subject: [PATCH 64/77] pip prod(deps): bump sphinx from 8.1.3 to 8.2.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.1.3 to 8.2.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.2.1/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.1.3...v8.2.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 118d14bbc..b599248f1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ sphinx_rtd_theme==3.0.2 -sphinx==8.1.3 +sphinx==8.2.1 sphinx-click==6.0.0 click prettytable From 4218d0b66a90f1646ccb2b49037ea59a196a908e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 21:42:07 +0000 Subject: [PATCH 65/77] pip prod(deps): bump sphinx from 8.2.1 to 8.2.3 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.2.1 to 8.2.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.2.1...v8.2.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index b599248f1..894d7abbc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ sphinx_rtd_theme==3.0.2 -sphinx==8.2.1 +sphinx==8.2.3 sphinx-click==6.0.0 click prettytable From a258e8d02f55652ee82de6167207835e57ff751d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:39:17 +0000 Subject: [PATCH 66/77] pip prod(deps): bump rich from 13.9.4 to 14.0.0 Bumps [rich](https://github.com/Textualize/rich) from 13.9.4 to 14.0.0. - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v13.9.4...v14.0.0) --- updated-dependencies: - dependency-name: rich dependency-version: 14.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- setup.py | 2 +- tools/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6e02544b2..6cb0bec25 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ 'prompt_toolkit >= 2', 'pygments >= 2.0.0', 'urllib3 >= 1.24', - 'rich == 13.9.4' + 'rich == 14.0.0' ], keywords=['softlayer', 'cloud', 'slcli', 'ibmcloud'], classifiers=[ diff --git a/tools/requirements.txt b/tools/requirements.txt index 335a26b20..f1d20e7a3 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -4,6 +4,6 @@ requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 -rich == 13.9.4 +rich == 14.0.0 # only used for soap transport # softlayer-zeep >= 5.0.0 From bd44eed02a5408336e88aff9e3109476090696b4 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 24 Apr 2025 15:27:49 -0500 Subject: [PATCH 67/77] #2219 Improved employee auth to work better on dev servers and fixed REST api auth --- SoftLayer/API.py | 11 +++++------ SoftLayer/CLI/login.py | 16 +++++++--------- SoftLayer/auth.py | 8 ++++---- SoftLayer/transports/rest.py | 6 +++++- SoftLayer/transports/transport.py | 14 +++++++++++++- docs/requirements.txt | 2 +- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index ea119a4b1..584c8fb50 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -46,7 +46,7 @@ 'raw_headers', 'limit', 'offset', - 'verify', + 'verify' )) @@ -182,7 +182,7 @@ def employee_client(username=None, verify=None, config_file=config_file) - url = settings.get('endpoint_url') + url = settings.get('endpoint_url', '') verify = settings.get('verify', True) if 'internal' not in url: @@ -374,7 +374,6 @@ def call(self, service, method, *args, **kwargs): request.url = self.settings['softlayer'].get('endpoint_url') if kwargs.get('verify') is not None: request.verify = kwargs.get('verify') - if self.auth: request = self.auth.get_request(request) @@ -495,7 +494,7 @@ def __setAuth(self, auth=None): """Prepares the authentication property""" if auth is None: auth_cert = self.settings['softlayer'].get('auth_cert') - serv_cert = self.settings['softlayer'].get('server_cert', None) + serv_cert = self.settings['softlayer'].get('verify', True) auth = slauth.X509Authentication(auth_cert, serv_cert) self.auth = auth @@ -712,8 +711,8 @@ def authenticate_with_internal(self, username, password, security_token=None): if len(security_token) != 6: raise exceptions.SoftLayerAPIError("Invalid security token: {}".format(security_token)) - auth_result = self.call('SoftLayer_User_Employee', 'performExternalAuthentication', - username, password, security_token) + self.auth = slauth.BasicHTTPAuthentication(username, password) + auth_result = self.call('SoftLayer_User_Employee', 'getEncryptedSessionToken', security_token) self.settings['softlayer']['access_token'] = auth_result['hash'] self.settings['softlayer']['userid'] = str(auth_result['userId']) diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 8ee981979..e725a165c 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -30,16 +30,15 @@ def cli(env): username = settings.get('username') or os.environ.get('SLCLI_USER', None) password = os.environ.get('SLCLI_PASSWORD', '') yubi = None - client = employee_client(config_file=env.config_file) # Might already be logged in, try and refresh token if settings.get('access_token') and settings.get('userid'): - client.authenticate_with_hash(settings.get('userid'), settings.get('access_token')) + env.client.authenticate_with_hash(settings.get('userid'), settings.get('access_token')) try: emp_id = settings.get('userid') - client.call('SoftLayer_User_Employee', 'getObject', id=emp_id, mask="mask[id,username]") - client.refresh_token(emp_id, settings.get('access_token')) - client.call('SoftLayer_User_Employee', 'refreshEncryptedToken', settings.get('access_token'), id=emp_id) + env.client.call('SoftLayer_User_Employee', 'getObject', id=emp_id, mask="mask[id,username]") + env.client.refresh_token(emp_id, settings.get('access_token')) + env.client.call('SoftLayer_User_Employee', 'refreshEncryptedToken', settings.get('access_token'), id=emp_id) config_settings['softlayer'] = settings config.write_config(config_settings, env.config_file) @@ -52,13 +51,12 @@ def cli(env): click.echo("URL: {}".format(url)) if username is None: username = input("Username: ") - click.echo("Username: {}".format(username)) if not password: - password = env.getpass("Password: ") - click.echo("Password: {}".format(censor_password(password))) + password = env.getpass("Password: ", default="") yubi = input("Yubi: ") + try: - result = client.authenticate_with_internal(username, password, str(yubi)) + result = env.client.authenticate_with_internal(username, password, str(yubi)) print(result) # pylint: disable=broad-exception-caught except Exception as e: diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 8698249ae..a0d283b14 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ - +import os __all__ = [ 'BasicAuthentication', @@ -89,7 +89,7 @@ def get_request(self, request): return request def __repr__(self): - return "BasicAuthentication(username=%r)" % self.username + return f"BasicAuthentication(username={self.username})" class BasicHTTPAuthentication(AuthenticationBase): @@ -110,7 +110,7 @@ def get_request(self, request): return request def __repr__(self): - return "BasicHTTPAuthentication(username=%r)" % self.username + return f"BasicHTTPAuthentication(username={self.username}" class BearerAuthentication(AuthenticationBase): @@ -149,7 +149,7 @@ class X509Authentication(AuthenticationBase): """ def __init__(self, cert, ca_cert): - self.cert = cert + self.cert = os.path.expanduser(cert) self.ca_cert = ca_cert def get_request(self, request): diff --git a/SoftLayer/transports/rest.py b/SoftLayer/transports/rest.py index 9d2a13269..2e2be1986 100644 --- a/SoftLayer/transports/rest.py +++ b/SoftLayer/transports/rest.py @@ -76,6 +76,9 @@ def __call__(self, request): request.params = params + # This handles any edge cases on the REST api. + request.special_rest_params() + auth = None if request.transport_user: auth = requests.auth.HTTPBasicAuth( @@ -110,7 +113,6 @@ def __call__(self, request): # Prefer the request setting, if it's not None if request.verify is None: request.verify = self.verify - try: resp = self.client.request(method, request.url, auth=auth, @@ -163,6 +165,8 @@ def print_reproduceable(request): :param request request: Request object """ + # This handles any edge cases on the REST api. + request.special_rest_params() command = "curl -u $SL_USER:$SL_APIKEY -X {method} -H {headers} {data} '{uri}'" method = REST_SPECIAL_METHODS.get(request.method) diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index 9496be50b..a5c3e6869 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -103,13 +103,25 @@ def __repr__(self): pretty_filter = self.filter clean_args = self.args # Passwords can show up here, so censor them before logging. - if self.method in ["performExternalAuthentication", "refreshEncryptedToken", "getPortalLoginToken"]: + if self.method in ["performExternalAuthentication", "refreshEncryptedToken", + "getPortalLoginToken", "getEncryptedSessionToken"]: clean_args = "*************" param_string = (f"id={self.identifier}, mask='{pretty_mask}', filter='{pretty_filter}', args={clean_args}, " f"limit={self.limit}, offset={self.offset}") return "{service}::{method}({params})".format( service=self.service, method=self.method, params=param_string) + def special_rest_params(self): + """This method is to handle the edge case of SoftLayer_User_Employee::getEncryptedSessionToken + + Added this method here since it was a little easier to change the data as needed this way. + """ + if len(self.args) == 0 or self.params: + return None + if self.method == "getEncryptedSessionToken" and self.service == "SoftLayer_User_Employee": + self.params = {"remoteToken": self.args[0]} + self.args = [] + class SoftLayerListResult(list): """A SoftLayer API list result.""" diff --git a/docs/requirements.txt b/docs/requirements.txt index b599248f1..894d7abbc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ sphinx_rtd_theme==3.0.2 -sphinx==8.2.1 +sphinx==8.2.3 sphinx-click==6.0.0 click prettytable From 6ea81c6af2f396f32bd1e5819d04aa0d308c621b Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 24 Apr 2025 16:12:28 -0500 Subject: [PATCH 68/77] #2219 fixed test errors --- SoftLayer/CLI/login.py | 1 - SoftLayer/auth.py | 2 +- SoftLayer/transports/transport.py | 5 ++++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index e725a165c..d37ea043c 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -4,7 +4,6 @@ import click -from SoftLayer.API import employee_client from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer import config diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index a0d283b14..cdbbf8526 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -89,7 +89,7 @@ def get_request(self, request): return request def __repr__(self): - return f"BasicAuthentication(username={self.username})" + return f"BasicAuthentication(username={self.username})" class BasicHTTPAuthentication(AuthenticationBase): diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index a5c3e6869..34cd643a9 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -44,6 +44,9 @@ def __init__(self): #: API Parameters. self.args = tuple() + #: URL Parameters, used for the REST Transport + self.params = None + #: API headers, used for authentication, masks, limits, offsets, etc. self.headers = {} @@ -117,7 +120,7 @@ def special_rest_params(self): Added this method here since it was a little easier to change the data as needed this way. """ if len(self.args) == 0 or self.params: - return None + return if self.method == "getEncryptedSessionToken" and self.service == "SoftLayer_User_Employee": self.params = {"remoteToken": self.args[0]} self.args = [] From 899de7514b4135c33ff73901c0aed2316891fb1a Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 24 Apr 2025 16:35:57 -0500 Subject: [PATCH 69/77] #2219 fixed an issue with how xmlrpc logs in employees --- SoftLayer/API.py | 4 ++-- SoftLayer/transports/transport.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 584c8fb50..69f0326ac 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -711,8 +711,8 @@ def authenticate_with_internal(self, username, password, security_token=None): if len(security_token) != 6: raise exceptions.SoftLayerAPIError("Invalid security token: {}".format(security_token)) - self.auth = slauth.BasicHTTPAuthentication(username, password) - auth_result = self.call('SoftLayer_User_Employee', 'getEncryptedSessionToken', security_token) + auth_result = self.call('SoftLayer_User_Employee', 'getEncryptedSessionToken', + username, password, security_token) self.settings['softlayer']['access_token'] = auth_result['hash'] self.settings['softlayer']['userid'] = str(auth_result['userId']) diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index 34cd643a9..e90249057 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -119,10 +119,12 @@ def special_rest_params(self): Added this method here since it was a little easier to change the data as needed this way. """ - if len(self.args) == 0 or self.params: - return if self.method == "getEncryptedSessionToken" and self.service == "SoftLayer_User_Employee": - self.params = {"remoteToken": self.args[0]} + if len(self.args) < 3: + return + self.params = {"remoteToken": self.args[2]} + self.transport_user = self.args[0] + self.transport_password = self.args[1] self.args = [] From ae07408d797f7ba94d5973ac2476ebc50b19e9d1 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 24 Apr 2025 16:43:15 -0500 Subject: [PATCH 70/77] Fixed a pylint issue --- SoftLayer/API.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 69f0326ac..cff277286 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -712,7 +712,7 @@ def authenticate_with_internal(self, username, password, security_token=None): raise exceptions.SoftLayerAPIError("Invalid security token: {}".format(security_token)) auth_result = self.call('SoftLayer_User_Employee', 'getEncryptedSessionToken', - username, password, security_token) + username, password, security_token) self.settings['softlayer']['access_token'] = auth_result['hash'] self.settings['softlayer']['userid'] = str(auth_result['userId']) From 10de21ca91953c7d942aa5e99e47f9689c825e95 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 9 May 2025 15:09:43 -0500 Subject: [PATCH 71/77] Update README-internal.md --- README-internal.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README-internal.md b/README-internal.md index df372ded0..5b25abda0 100644 --- a/README-internal.md +++ b/README-internal.md @@ -11,6 +11,12 @@ On Mac, after installing the softlayer.local certificate, the following worked f security export -t certs -f pemseq -k /System/Library/Keychains/SystemRootCertificates.keychain -o bundleCA.pem sudo cp bundleCA.pem /etc/ssl/certs/bundleCA.pem ``` +Alternatively +```bash +API_HOST= +echo quit | openssl s_client -showcerts -servername "${API_HOST}" -connect "${API_HOST}":443 > cacert.pem +``` + Then in the `~/.softlayer` config, set `verify = /etc/ssl/certs/bundleCA.pem` and that should work. You may also need to set `REQUESTS_CA_BUNDLE` -> `export REQUESTS_CA_BUNDLE=/etc/ssl/certs/bundleCA.pem` to force python to load your CA bundle From d92d20a97ac8e65524ccbfd7487d9c8899948544 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Thu, 22 May 2025 15:29:56 +0200 Subject: [PATCH 72/77] Support Click 8.2+ Click 8.2 and above will now force an abort if a confirmation prompt isn't answered, rather than raising the CLIAbort that is expected. Catch this exception so that our own exceptions are raised. --- SoftLayer/CLI/formatting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index b9eca571e..b4c9a98f7 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -260,7 +260,10 @@ def no_going_back(confirmation): prompt = f"This action cannot be undone! Type '{confirmation}' or press Enter to abort" - ans = click.prompt(prompt, default='', show_default=False) + try: + ans = click.prompt(prompt, default='', show_default=False) + except click.exceptions.Abort: + return False if ans.lower() == str(confirmation).lower(): return True From 3947b27f90ba556bde7dc73abd13802362f3b219 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 23 May 2025 09:21:58 +0530 Subject: [PATCH 73/77] Fixed tox errors --- SoftLayer/CLI/command.py | 5 +++++ SoftLayer/CLI/object_storage/credential/__init__.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/SoftLayer/CLI/command.py b/SoftLayer/CLI/command.py index b4cbd768c..34c50e549 100644 --- a/SoftLayer/CLI/command.py +++ b/SoftLayer/CLI/command.py @@ -73,6 +73,7 @@ def get_command(self, ctx, cmd_name): else: return module + # # pylint: disable=unused-argument def format_usage(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None: """Formats and colorizes the usage information.""" self.ensure_env(ctx) @@ -85,6 +86,7 @@ def format_usage(self, ctx: click.Context, formatter: click.formatting.HelpForma self.console.print(f"Usage: [path]{ctx.command_path}[/] {' '.join(pieces)}") + # pylint: disable=unused-argument def format_help_text(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None: """Writes the help text""" text = self.help if self.help is not None else "" @@ -104,6 +106,7 @@ def format_epilog(self, ctx: click.Context, formatter: click.formatting.HelpForm self.console.print(epilog) self.format_commands(ctx, formatter) + # pylint: disable=unused-argument def format_options(self, ctx, formatter): """Prints out the options in a table format""" @@ -136,7 +139,9 @@ def format_options(self, ctx, formatter): self.console.print(options_table) + # pylint: disable=unused-argument def format_commands(self, ctx, formatter): + """Formats the command list for click""" commands = [] for subcommand in self.list_commands(ctx): cmd = self.get_command(ctx, subcommand) diff --git a/SoftLayer/CLI/object_storage/credential/__init__.py b/SoftLayer/CLI/object_storage/credential/__init__.py index f3bc2723a..82bfef311 100644 --- a/SoftLayer/CLI/object_storage/credential/__init__.py +++ b/SoftLayer/CLI/object_storage/credential/__init__.py @@ -17,6 +17,7 @@ def __init__(self, **attrs): click.MultiCommand.__init__(self, **attrs) self.path = os.path.dirname(__file__) + # pylint: disable=unused-argument def list_commands(self, ctx): """List all sub-commands.""" commands = [] @@ -28,6 +29,7 @@ def list_commands(self, ctx): commands.sort() return commands + # pylint: disable=unused-argument def get_command(self, ctx, cmd_name): """Get command for click.""" path = "%s.%s" % (__name__, cmd_name) From bf8bf3b4ce299d6783e35efe328bfff1d7532b68 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Fri, 23 May 2025 12:33:51 +0200 Subject: [PATCH 74/77] Support Click 8.2+ Click 8.2 and above will now force an abort if a confirmation prompt isn't answered, rather than raising the CLIAbort that is expected. Catch this exception so that our own exceptions are raised. --- SoftLayer/CLI/formatting.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index b9eca571e..2c0e324fe 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -243,7 +243,10 @@ def confirm(prompt_str, default=False): default_str = 'n' prompt = '%s [y/N]' % prompt_str - ans = click.prompt(prompt, default=default_str, show_default=False) + try: + ans = click.prompt(prompt, default=default_str, show_default=False) + except click.exceptions.Abort: + return False if ans.lower() in ('y', 'yes', 'yeah', 'yup', 'yolo'): return True @@ -260,7 +263,10 @@ def no_going_back(confirmation): prompt = f"This action cannot be undone! Type '{confirmation}' or press Enter to abort" - ans = click.prompt(prompt, default='', show_default=False) + try: + ans = click.prompt(prompt, default='', show_default=False) + except click.exceptions.Abort: + return False if ans.lower() == str(confirmation).lower(): return True From 54775a739b686852fece684e461f70d1f2d19fa5 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 11 Jun 2025 16:28:38 -0500 Subject: [PATCH 75/77] Support for NVMe Dual Raid card ordering. Added KeyName and Category code to prices for easier troublehsooting. Fixed #2225 --- .secrets.baseline | 4 +-- SoftLayer/managers/ordering.py | 41 ++++++++++++++++++++------ tests/managers/ordering_tests.py | 18 +++++------ tests/managers/vs/vs_capacity_tests.py | 14 ++++++--- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 394061815..f0aee0650 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2025-02-14T20:05:29Z", + "generated_at": "2025-06-11T21:28:32Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -700,7 +700,7 @@ "hashed_secret": "8af1f8146d96a3cd862281442d0d6c5cb6f8f9e5", "is_secret": false, "is_verified": false, - "line_number": 181, + "line_number": 187, "type": "Hex High Entropy String", "verified_result": null } diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 8a17455bd..3a56d69b5 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -357,10 +357,14 @@ def get_preset_by_key(self, package_keyname, preset_keyname, mask=None): if len(presets) == 0: raise exceptions.SoftLayerError( f"Preset {preset_keyname} does not exist in package {package_keyname}") - return presets[0] def get_price_id_list(self, package_keyname, item_keynames, core=None): + """Returns just a list of price IDs for backwards compatability""" + prices = self.get_ordering_prices(package_keyname, item_keynames, core) + return [price.get('id') for price in prices] + + def get_ordering_prices(self, package_keyname: str, item_keynames: list, core=None) -> list: """Converts a list of item keynames to a list of price IDs. This function is used to convert a list of item keynames into @@ -370,8 +374,7 @@ def get_price_id_list(self, package_keyname, item_keynames, core=None): :param str package_keyname: The package associated with the prices :param list item_keynames: A list of item keyname strings :param str core: preset guest core capacity. - :returns: A list of price IDs associated with the given item - keynames in the given package + :returns: A list of price IDs associated with the given item keynames in the given package """ mask = 'id, description, capacity, itemCategory, keyName, prices[categories], ' \ @@ -380,7 +383,8 @@ def get_price_id_list(self, package_keyname, item_keynames, core=None): item_capacity = self.get_item_capacity(items, item_keynames) prices = [] - category_dict = {"gpu0": -1, "pcie_slot0": -1} + # start at -1 so we can increment before we use it. 0 is a valid value here + category_dict = {"gpu0": -1, "pcie_slot0": -1, "disk_controller": -1} for item_keyname in item_keynames: matching_item = [] @@ -410,15 +414,33 @@ def get_price_id_list(self, package_keyname, item_keynames, core=None): # GPU and PCIe items has two generic prices and they are added to the list # according to the number of items in the order. category_dict[item_category] += 1 - category_code = item_category[:-1] + str(category_dict[item_category]) + item_category = self.get_special_category(category_dict[item_category], item_category) + price_id = [p['id'] for p in matching_item['prices'] if not p['locationGroupId'] - and p['categories'][0]['categoryCode'] == category_code][0] + and p['categories'][0]['categoryCode'] == item_category][0] - prices.append(price_id) + prices.append({ + "id": price_id, + "categories": [{"categoryCode": item_category}], + "item": {"keyName": item_keyname} + }) return prices + @staticmethod + def get_special_category(index: int, base: str) -> str: + """Handles cases where we need to find price on a special category price id""" + # disk_controller and disk_controller1 + if base == "disk_controller": + if index == 0: + return base + else: + return f"{base}1" + + # gpu0 and gpu1, pcie_slot0 and pcie_slot1 + return base[:-1] + str(index) + @staticmethod def get_item_price_id(core, prices, term=0): """get item price id @@ -644,8 +666,9 @@ def generate_order(self, package_keyname, location, item_keynames, complex_type= raise exceptions.SoftLayerError("A complex type must be specified with the order") order['complexType'] = complex_type - price_ids = self.get_price_id_list(package_keyname, item_keynames, preset_core) - order['prices'] = [{'id': price_id} for price_id in price_ids] + order['prices'] = self.get_ordering_prices(package_keyname, item_keynames, preset_core) + # price_ids = self.get_price_id_list(package_keyname, item_keynames, preset_core) + # order['prices'] = [{'id': price_id} for price_id in price_ids] container['orderContainers'] = [order] diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py index 7d86c0051..db4f85988 100644 --- a/tests/managers/ordering_tests.py +++ b/tests/managers/ordering_tests.py @@ -435,13 +435,13 @@ def test_generate_order(self): pkg = 'PACKAGE_KEYNAME' items = ['ITEM1', 'ITEM2'] complex_type = 'My_Type' - expected_order = {'orderContainers': [ - {'complexType': 'My_Type', - 'location': 1854895, - 'packageId': 1234, - 'prices': [{'id': 1111}, {'id': 2222}], - 'quantity': 1, - 'useHourlyPricing': True} + expected_order = {'orderContainers': [{ + 'complexType': 'My_Type', + 'location': 1854895, + 'packageId': 1234, + 'prices': [{'id': 1111}, {'id': 2222}], + 'quantity': 1, + 'useHourlyPricing': True} ]} mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() @@ -568,7 +568,7 @@ def _patch_for_generate(self): # with patchers mock_pkg = mock.patch.object(self.ordering, 'get_package_by_key') mock_preset = mock.patch.object(self.ordering, 'get_preset_by_key') - mock_get_ids = mock.patch.object(self.ordering, 'get_price_id_list') + mock_get_ids = mock.patch.object(self.ordering, 'get_ordering_prices') # start each patcher, and set a cleanup to stop each patcher as well to_return = [] @@ -579,7 +579,7 @@ def _patch_for_generate(self): # set the return values on each of the mocks to_return[0].return_value = {'id': 1234} to_return[1].return_value = {'id': 5678} - to_return[2].return_value = [1111, 2222] + to_return[2].return_value = [{'id': 1111}, {'id': 2222}] return to_return def test_get_location_id_short(self): diff --git a/tests/managers/vs/vs_capacity_tests.py b/tests/managers/vs/vs_capacity_tests.py index fbebc55a5..6e4dd01a8 100644 --- a/tests/managers/vs/vs_capacity_tests.py +++ b/tests/managers/vs/vs_capacity_tests.py @@ -77,8 +77,11 @@ def test_create(self): 'quantity': 5, 'useHourlyPricing': True, 'complexType': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', - 'prices': [{'id': 217561} - ] + 'prices': [{ + 'id': 217561, + 'categories': [{'categoryCode': 'reserved_capacity'}], + 'item': {'keyName': 'B1_1X2_1_YEAR_TERM'} + }] } ] } @@ -103,8 +106,11 @@ def test_create_test(self): 'quantity': 5, 'useHourlyPricing': True, 'complexType': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', - 'prices': [{'id': 217561}], - + 'prices': [{ + 'id': 217561, + 'categories': [{'categoryCode': 'reserved_capacity'}], + 'item': {'keyName': 'B1_1X2_1_YEAR_TERM'} + }] } ] } From 1119216b9bfb8e800d54b6583fcf47ffb649ffc3 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 16 Jun 2025 17:18:37 -0500 Subject: [PATCH 76/77] Removed IPSec VPN tunnel commands. #2227 --- SoftLayer/CLI/routes.py | 13 - SoftLayer/CLI/vpn/__init__.py | 1 - SoftLayer/CLI/vpn/ipsec/__init__.py | 1 - SoftLayer/CLI/vpn/ipsec/cancel.py | 39 -- SoftLayer/CLI/vpn/ipsec/configure.py | 29 - SoftLayer/CLI/vpn/ipsec/detail.py | 176 ------ SoftLayer/CLI/vpn/ipsec/list.py | 35 -- SoftLayer/CLI/vpn/ipsec/order.py | 34 -- SoftLayer/CLI/vpn/ipsec/subnet/__init__.py | 1 - SoftLayer/CLI/vpn/ipsec/subnet/add.py | 76 --- SoftLayer/CLI/vpn/ipsec/subnet/remove.py | 50 -- .../CLI/vpn/ipsec/translation/__init__.py | 1 - SoftLayer/CLI/vpn/ipsec/translation/add.py | 41 -- SoftLayer/CLI/vpn/ipsec/translation/remove.py | 33 -- SoftLayer/CLI/vpn/ipsec/translation/update.py | 46 -- SoftLayer/CLI/vpn/ipsec/update.py | 101 ---- SoftLayer/managers/ipsec.py | 317 ---------- tests/CLI/modules/ipsec_tests.py | 559 ------------------ tests/managers/ipsec_tests.py | 327 ---------- 19 files changed, 1880 deletions(-) delete mode 100644 SoftLayer/CLI/vpn/__init__.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/__init__.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/cancel.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/configure.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/detail.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/list.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/order.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/subnet/__init__.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/subnet/add.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/subnet/remove.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/translation/__init__.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/translation/add.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/translation/remove.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/translation/update.py delete mode 100644 SoftLayer/CLI/vpn/ipsec/update.py delete mode 100644 SoftLayer/managers/ipsec.py delete mode 100644 tests/CLI/modules/ipsec_tests.py delete mode 100644 tests/managers/ipsec_tests.py diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index c2d517e2d..c1dcc4b7d 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -217,19 +217,6 @@ ('image:share', 'SoftLayer.CLI.image.share:cli'), ('image:share-deny', 'SoftLayer.CLI.image.share_deny:cli'), - ('ipsec', 'SoftLayer.CLI.vpn.ipsec'), - ('ipsec:configure', 'SoftLayer.CLI.vpn.ipsec.configure:cli'), - ('ipsec:detail', 'SoftLayer.CLI.vpn.ipsec.detail:cli'), - ('ipsec:list', 'SoftLayer.CLI.vpn.ipsec.list:cli'), - ('ipsec:subnet-add', 'SoftLayer.CLI.vpn.ipsec.subnet.add:cli'), - ('ipsec:subnet-remove', 'SoftLayer.CLI.vpn.ipsec.subnet.remove:cli'), - ('ipsec:translation-add', 'SoftLayer.CLI.vpn.ipsec.translation.add:cli'), - ('ipsec:translation-remove', 'SoftLayer.CLI.vpn.ipsec.translation.remove:cli'), - ('ipsec:translation-update', 'SoftLayer.CLI.vpn.ipsec.translation.update:cli'), - ('ipsec:update', 'SoftLayer.CLI.vpn.ipsec.update:cli'), - ('ipsec:order', 'SoftLayer.CLI.vpn.ipsec.order:cli'), - ('ipsec:cancel', 'SoftLayer.CLI.vpn.ipsec.cancel:cli'), - ('loadbal', 'SoftLayer.CLI.loadbal'), ('loadbal:detail', 'SoftLayer.CLI.loadbal.detail:cli'), ('loadbal:list', 'SoftLayer.CLI.loadbal.list:cli'), diff --git a/SoftLayer/CLI/vpn/__init__.py b/SoftLayer/CLI/vpn/__init__.py deleted file mode 100644 index a61d51191..000000000 --- a/SoftLayer/CLI/vpn/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Virtual Private Networks""" diff --git a/SoftLayer/CLI/vpn/ipsec/__init__.py b/SoftLayer/CLI/vpn/ipsec/__init__.py deleted file mode 100644 index 72e48782c..000000000 --- a/SoftLayer/CLI/vpn/ipsec/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""IPSEC VPN""" diff --git a/SoftLayer/CLI/vpn/ipsec/cancel.py b/SoftLayer/CLI/vpn/ipsec/cancel.py deleted file mode 100644 index 05688a106..000000000 --- a/SoftLayer/CLI/vpn/ipsec/cancel.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Cancel an IPSec service.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('identifier') -@click.option('--immediate', - is_flag=True, - default=False, - help="Cancels the service immediately (instead of on the billing anniversary)") -@click.option('--reason', - help="An optional cancellation reason. See cancel-reasons for a list of available options") -@click.option('--force', default=False, is_flag=True, help="Force cancel ipsec vpn without confirmation") -@environment.pass_env -def cli(env, identifier, immediate, reason, force): - """Cancel a IPSEC VPN tunnel context.""" - - manager = SoftLayer.IPSECManager(env.client) - context = manager.get_tunnel_context(identifier, mask='billingItem') - - if 'billingItem' not in context: - raise SoftLayer.SoftLayerError("Cannot locate billing. May already be cancelled.") - - if not force: - if not (env.skip_confirmations or - formatting.confirm("This will cancel the Ipsec Vpn and cannot be undone. Continue?")): - raise exceptions.CLIAbort('Aborted') - - result = manager.cancel_item(context['billingItem']['id'], immediate, reason) - - if result: - env.fout(f"Ipsec {identifier} was cancelled.") diff --git a/SoftLayer/CLI/vpn/ipsec/configure.py b/SoftLayer/CLI/vpn/ipsec/configure.py deleted file mode 100644 index 7fe0ea456..000000000 --- a/SoftLayer/CLI/vpn/ipsec/configure.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Request network configuration of an IPSEC tunnel context.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI.exceptions import CLIHalt - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@environment.pass_env -def cli(env, context_id): - """Request configuration of a tunnel context. - - This action will update the advancedConfigurationFlag on the context - instance and further modifications against the context will be prevented - until all changes can be propgated to network devices. - """ - manager = SoftLayer.IPSECManager(env.client) - # ensure context can be retrieved by given id - manager.get_tunnel_context(context_id) - - succeeded = manager.apply_configuration(context_id) - if succeeded: - click.echo(f'Configuration request received for context #{context_id}') - else: - raise CLIHalt(f'Failed to enqueue configuration request for context #{context_id}') diff --git a/SoftLayer/CLI/vpn/ipsec/detail.py b/SoftLayer/CLI/vpn/ipsec/detail.py deleted file mode 100644 index 64c06eefb..000000000 --- a/SoftLayer/CLI/vpn/ipsec/detail.py +++ /dev/null @@ -1,176 +0,0 @@ -"""List IPSEC VPN Tunnel Context Details.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@click.option('-i', - '--include', - default=[], - multiple=True, - type=click.Choice(['at', 'is', 'rs', 'sr', 'ss']), - help='Include additional resources') -@environment.pass_env -def cli(env, context_id, include): - """List IPSEC VPN tunnel context details. - - Additional resources can be joined using multiple instances of the - include option, for which the following choices are available. - - \b - at: address translations - is: internal subnets - rs: remote subnets - sr: statically routed subnets - ss: service subnets - """ - mask = _get_tunnel_context_mask(('at' in include), - ('is' in include), - ('rs' in include), - ('sr' in include), - ('ss' in include)) - manager = SoftLayer.IPSECManager(env.client) - context = manager.get_tunnel_context(context_id, mask=mask) - - env.fout(_get_context_table(context)) - - for relation in include: - if relation == 'at': - env.fout(_get_address_translations_table(context.get('addressTranslations', []))) - elif relation == 'is': - env.fout(_get_subnets_table(context.get('internalSubnets', []), title="Internal Subnets")) - elif relation == 'rs': - env.fout(_get_subnets_table(context.get('customerSubnets', []), title="Remote Subnets")) - elif relation == 'sr': - env.fout(_get_subnets_table(context.get('staticRouteSubnets', []), title="Static Subnets")) - elif relation == 'ss': - env.fout(_get_subnets_table(context.get('serviceSubnets', []), title="Service Subnets")) - - -def _get_address_translations_table(address_translations): - """Yields a formatted table to print address translations. - - :param List[dict] address_translations: List of address translations. - :return Table: Formatted for address translation output. - """ - table = formatting.Table(['id', - 'static IP address', - 'static IP address id', - 'remote IP address', - 'remote IP address id', - 'note'], title="Address Translations") - for address_translation in address_translations: - table.add_row([address_translation.get('id', ''), - address_translation.get('internalIpAddressRecord', {}).get('ipAddress', ''), - address_translation.get('internalIpAddressId', ''), - address_translation.get('customerIpAddressRecord', {}).get('ipAddress', ''), - address_translation.get('customerIpAddressId', ''), - address_translation.get('notes', '')]) - return table - - -def _get_subnets_table(subnets, title): - """Yields a formatted table to print subnet details. - - :param List[dict] subnets: List of subnets. - :return Table: Formatted for subnet output. - """ - table = formatting.Table(['id', 'network identifier', 'cidr', 'note'], title=title) - for subnet in subnets: - table.add_row([subnet.get('id', ''), - subnet.get('networkIdentifier', ''), - subnet.get('cidr', ''), - subnet.get('note', '')]) - return table - - -def _get_tunnel_context_mask(address_translations=False, - internal_subnets=False, - remote_subnets=False, - static_subnets=False, - service_subnets=False): - """Yields a mask object for a tunnel context. - - All exposed properties on the tunnel context service are included in - the constructed mask. Additional joins may be requested. - - :param bool address_translations: Whether to join the context's address - translation entries. - :param bool internal_subnets: Whether to join the context's internal - subnet associations. - :param bool remote_subnets: Whether to join the context's remote subnet - associations. - :param bool static_subnets: Whether to join the context's statically - routed subnet associations. - :param bool service_subnets: Whether to join the SoftLayer service - network subnets. - :return string: Encoding for the requested mask object. - """ - entries = ['id', - 'accountId', - 'advancedConfigurationFlag', - 'createDate', - 'customerPeerIpAddress', - 'modifyDate', - 'name', - 'friendlyName', - 'internalPeerIpAddress', - 'phaseOneAuthentication', - 'phaseOneDiffieHellmanGroup', - 'phaseOneEncryption', - 'phaseOneKeylife', - 'phaseTwoAuthentication', - 'phaseTwoDiffieHellmanGroup', - 'phaseTwoEncryption', - 'phaseTwoKeylife', - 'phaseTwoPerfectForwardSecrecy', - 'presharedKey'] - if address_translations: - entries.append('addressTranslations[internalIpAddressRecord[ipAddress],' - 'customerIpAddressRecord[ipAddress]]') - if internal_subnets: - entries.append('internalSubnets') - if remote_subnets: - entries.append('customerSubnets') - if static_subnets: - entries.append('staticRouteSubnets') - if service_subnets: - entries.append('serviceSubnets') - return f"[mask[{','.join(entries)}]]" - - -def _get_context_table(context): - """Yields a formatted table to print context details. - - :param dict context: The tunnel context - :return Table: Formatted for tunnel context output - """ - table = formatting.KeyValueTable(['name', 'value'], title='Context Details') - table.align['name'] = 'r' - table.align['value'] = 'l' - - table.add_row(['id', context.get('id', '')]) - table.add_row(['name', context.get('name', '')]) - table.add_row(['friendly name', context.get('friendlyName', '')]) - table.add_row(['internal peer IP address', context.get('internalPeerIpAddress', '')]) - table.add_row(['remote peer IP address', context.get('customerPeerIpAddress', '')]) - table.add_row(['advanced configuration flag', context.get('advancedConfigurationFlag', '')]) - table.add_row(['preshared key', context.get('presharedKey', '')]) - table.add_row(['phase 1 authentication', context.get('phaseOneAuthentication', '')]) - table.add_row(['phase 1 diffie hellman group', context.get('phaseOneDiffieHellmanGroup', '')]) - table.add_row(['phase 1 encryption', context.get('phaseOneEncryption', '')]) - table.add_row(['phase 1 key life', context.get('phaseOneKeylife', '')]) - table.add_row(['phase 2 authentication', context.get('phaseTwoAuthentication', '')]) - table.add_row(['phase 2 diffie hellman group', context.get('phaseTwoDiffieHellmanGroup', '')]) - table.add_row(['phase 2 encryption', context.get('phaseTwoEncryption', '')]) - table.add_row(['phase 2 key life', context.get('phaseTwoKeylife', '')]) - table.add_row(['phase 2 perfect forward secrecy', context.get('phaseTwoPerfectForwardSecrecy', '')]) - table.add_row(['created', context.get('createDate')]) - table.add_row(['modified', context.get('modifyDate')]) - return table diff --git a/SoftLayer/CLI/vpn/ipsec/list.py b/SoftLayer/CLI/vpn/ipsec/list.py deleted file mode 100644 index 652bd8330..000000000 --- a/SoftLayer/CLI/vpn/ipsec/list.py +++ /dev/null @@ -1,35 +0,0 @@ -"""List IPSec VPN Tunnel Contexts.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.option('--sortby', help='Column to sort by', - default='created') -@environment.pass_env -def cli(env, sortby): - """List IPSec VPN tunnel contexts""" - manager = SoftLayer.IPSECManager(env.client) - contexts = manager.get_tunnel_contexts() - - table = formatting.Table(['id', - 'name', - 'friendly name', - 'internal peer IP address', - 'remote peer IP address', - 'created']) - table.sortby = sortby - - for context in contexts: - table.add_row([context.get('id', ''), - context.get('name', ''), - context.get('friendlyName', ''), - context.get('internalPeerIpAddress', ''), - context.get('customerPeerIpAddress', ''), - context.get('createDate', '')]) - env.fout(table) diff --git a/SoftLayer/CLI/vpn/ipsec/order.py b/SoftLayer/CLI/vpn/ipsec/order.py deleted file mode 100644 index 2ca6413ef..000000000 --- a/SoftLayer/CLI/vpn/ipsec/order.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Order a IPSec VPN tunnel.""" -# :licenses: MIT, see LICENSE for more details. - -import click - -import SoftLayer - -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions -from SoftLayer.CLI import formatting - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.option('--datacenter', '-d', required=True, prompt=True, help="Datacenter shortname") -@environment.pass_env -def cli(env, datacenter): - """Order/create a IPSec VPN tunnel instance.""" - - ipsec_manager = SoftLayer.IPSECManager(env.client) - - if not (env.skip_confirmations or formatting.confirm( - "This action will incur charges on your account. Continue?")): - raise exceptions.CLIAbort('Aborting ipsec order.') - - result = ipsec_manager.order(datacenter, ['IPSEC_STANDARD']) - - table = formatting.KeyValueTable(['Name', 'Value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - table.add_row(['Id', result['orderId']]) - table.add_row(['Created', result['orderDate']]) - table.add_row(['Name', result['placedOrder']['items'][0]['description']]) - - env.fout(table) diff --git a/SoftLayer/CLI/vpn/ipsec/subnet/__init__.py b/SoftLayer/CLI/vpn/ipsec/subnet/__init__.py deleted file mode 100644 index 5ec029c51..000000000 --- a/SoftLayer/CLI/vpn/ipsec/subnet/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""IPSEC VPN Subnets""" diff --git a/SoftLayer/CLI/vpn/ipsec/subnet/add.py b/SoftLayer/CLI/vpn/ipsec/subnet/add.py deleted file mode 100644 index c9e1f56ab..000000000 --- a/SoftLayer/CLI/vpn/ipsec/subnet/add.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Add a subnet to an IPSEC tunnel context.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI.custom_types import NetworkParamType -from SoftLayer.CLI import environment -from SoftLayer.CLI.exceptions import ArgumentError -from SoftLayer.CLI.exceptions import CLIHalt - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@click.option('-s', - '--subnet-id', - default=None, - type=int, - help='Subnet identifier to add') -@click.option('-t', - '--subnet-type', - '--type', - required=True, - type=click.Choice(['internal', 'remote', 'service']), - help='Subnet type to add') -@click.option('-n', - '--network-identifier', - '--network', - default=None, - type=NetworkParamType(), - help='Subnet network identifier to create') -@environment.pass_env -def cli(env, context_id, subnet_id, subnet_type, network_identifier): - """Add a subnet to an IPSEC tunnel context. - - A subnet id may be specified to link to the existing tunnel context. - - Otherwise, a network identifier in CIDR notation should be specified, - indicating that a subnet resource should first be created before associating - it with the tunnel context. Note that this is only supported for remote - subnets, which are also deleted upon failure to attach to a context. - - A separate configuration request should be made to realize changes on - network devices. - """ - create_remote = False - if subnet_id is None: - if network_identifier is None: - raise ArgumentError('Either a network identifier or subnet id ' - 'must be provided.') - if subnet_type != 'remote': - raise ArgumentError(f'Unable to create {subnet_type} subnets') - create_remote = True - - manager = SoftLayer.IPSECManager(env.client) - context = manager.get_tunnel_context(context_id) - - if create_remote: - subnet = manager.create_remote_subnet(context['accountId'], - identifier=network_identifier[0], - cidr=network_identifier[1]) - subnet_id = subnet['id'] - env.out(f'Created subnet {network_identifier[0]}/{network_identifier[1]} #{subnet_id}') - - succeeded = False - if subnet_type == 'internal': - succeeded = manager.add_internal_subnet(context_id, subnet_id) - elif subnet_type == 'remote': - succeeded = manager.add_remote_subnet(context_id, subnet_id) - elif subnet_type == 'service': - succeeded = manager.add_service_subnet(context_id, subnet_id) - - if succeeded: - env.out(f'Added {subnet_type} subnet #{subnet_id}') - else: - raise CLIHalt(f'Failed to add {subnet_type} subnet #{subnet_id}') diff --git a/SoftLayer/CLI/vpn/ipsec/subnet/remove.py b/SoftLayer/CLI/vpn/ipsec/subnet/remove.py deleted file mode 100644 index 96ae253ba..000000000 --- a/SoftLayer/CLI/vpn/ipsec/subnet/remove.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Remove a subnet from an IPSEC tunnel context.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI.exceptions import CLIHalt - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@click.option('-s', - '--subnet-id', - required=True, - type=int, - help='Subnet identifier to remove') -@click.option('-t', - '--subnet-type', - '--type', - required=True, - type=click.Choice(['internal', 'remote', 'service']), - help='Subnet type to add') -@environment.pass_env -def cli(env, context_id, subnet_id, subnet_type): - """Remove a subnet from an IPSEC tunnel context. - - The subnet id to remove must be specified. - - Remote subnets are deleted upon removal from a tunnel context. - - A separate configuration request should be made to realize changes on - network devices. - """ - manager = SoftLayer.IPSECManager(env.client) - # ensure context can be retrieved by given id - manager.get_tunnel_context(context_id) - - succeeded = False - if subnet_type == 'internal': - succeeded = manager.remove_internal_subnet(context_id, subnet_id) - elif subnet_type == 'remote': - succeeded = manager.remove_remote_subnet(context_id, subnet_id) - elif subnet_type == 'service': - succeeded = manager.remove_service_subnet(context_id, subnet_id) - - if succeeded: - env.out(f'Removed {subnet_type} subnet #{subnet_id}') - else: - raise CLIHalt(f'Failed to remove {subnet_type} subnet #{subnet_id}') diff --git a/SoftLayer/CLI/vpn/ipsec/translation/__init__.py b/SoftLayer/CLI/vpn/ipsec/translation/__init__.py deleted file mode 100644 index 18be25229..000000000 --- a/SoftLayer/CLI/vpn/ipsec/translation/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""IPSEC VPN Address Translations""" diff --git a/SoftLayer/CLI/vpn/ipsec/translation/add.py b/SoftLayer/CLI/vpn/ipsec/translation/add.py deleted file mode 100644 index 196a6c429..000000000 --- a/SoftLayer/CLI/vpn/ipsec/translation/add.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Add an address translation to an IPSEC tunnel context.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -# from SoftLayer.CLI.exceptions import ArgumentError -# from SoftLayer.CLI.exceptions import CLIHalt - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@click.option('-s', - '--static-ip', - required=True, - help='Static IP address value') -@click.option('-r', - '--remote-ip', - required=True, - help='Remote IP address value') -@click.option('-n', - '--note', - default=None, - help='Note value') -@environment.pass_env -def cli(env, context_id, static_ip, remote_ip, note): - """Add an address translation to an IPSEC tunnel context. - - A separate configuration request should be made to realize changes on - network devices. - """ - manager = SoftLayer.IPSECManager(env.client) - # ensure context can be retrieved by given id - manager.get_tunnel_context(context_id) - - translation = manager.create_translation(context_id, - static_ip=static_ip, - remote_ip=remote_ip, - notes=note) - env.out(f"Created translation from {static_ip} to {remote_ip} #{translation['id']}") diff --git a/SoftLayer/CLI/vpn/ipsec/translation/remove.py b/SoftLayer/CLI/vpn/ipsec/translation/remove.py deleted file mode 100644 index e9ab43fdb..000000000 --- a/SoftLayer/CLI/vpn/ipsec/translation/remove.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Remove a translation entry from an IPSEC tunnel context.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI.exceptions import CLIHalt - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@click.option('-t', - '--translation-id', - required=True, - type=int, - help='Translation identifier to remove') -@environment.pass_env -def cli(env, context_id, translation_id): - """Remove a translation entry from an IPSEC tunnel context. - - A separate configuration request should be made to realize changes on - network devices. - """ - manager = SoftLayer.IPSECManager(env.client) - # ensure translation can be retrieved by given id - manager.get_translation(context_id, translation_id) - - succeeded = manager.remove_translation(context_id, translation_id) - if succeeded: - env.out(f'Removed translation #{translation_id}') - else: - raise CLIHalt(f'Failed to remove translation #{translation_id}') diff --git a/SoftLayer/CLI/vpn/ipsec/translation/update.py b/SoftLayer/CLI/vpn/ipsec/translation/update.py deleted file mode 100644 index bc2b1563d..000000000 --- a/SoftLayer/CLI/vpn/ipsec/translation/update.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Update an address translation for an IPSEC tunnel context.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI.exceptions import CLIHalt - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@click.option('-t', - '--translation-id', - required=True, - type=int, - help='Translation identifier to update') -@click.option('-s', - '--static-ip', - default=None, - help='Static IP address value') -@click.option('-r', - '--remote-ip', - default=None, - help='Remote IP address value') -@click.option('-n', - '--note', - default=None, - help='Note value') -@environment.pass_env -def cli(env, context_id, translation_id, static_ip, remote_ip, note): - """Update an address translation for an IPSEC tunnel context. - - A separate configuration request should be made to realize changes on - network devices. - """ - manager = SoftLayer.IPSECManager(env.client) - succeeded = manager.update_translation(context_id, - translation_id, - static_ip=static_ip, - remote_ip=remote_ip, - notes=note) - if succeeded: - env.out(f'Updated translation #{translation_id}') - else: - raise CLIHalt(f'Failed to update translation #{translation_id}') diff --git a/SoftLayer/CLI/vpn/ipsec/update.py b/SoftLayer/CLI/vpn/ipsec/update.py deleted file mode 100644 index 8a1e6f7a9..000000000 --- a/SoftLayer/CLI/vpn/ipsec/update.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Updates an IPSEC tunnel context.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI.exceptions import CLIHalt - - -@click.command(cls=SoftLayer.CLI.command.SLCommand, ) -@click.argument('context_id', type=int) -@click.option('--friendly-name', - default=None, - help='Friendly name value') -@click.option('--remote-peer', - default=None, - help='Remote peer IP address value') -@click.option('--preshared-key', - default=None, - help='Preshared key value') -@click.option('--phase1-auth', - '--p1-auth', - default=None, - type=click.Choice(['MD5', 'SHA1', 'SHA256']), - help='Phase 1 authentication value') -@click.option('--phase1-crypto', - '--p1-crypto', - default=None, - type=click.Choice(['DES', '3DES', 'AES128', 'AES192', 'AES256']), - help='Phase 1 encryption value') -@click.option('--phase1-dh', - '--p1-dh', - default=None, - type=click.Choice(['0', '1', '2', '5']), - help='Phase 1 diffie hellman group value') -@click.option('--phase1-key-ttl', - '--p1-key-ttl', - default=None, - type=click.IntRange(120, 172800), - help='Phase 1 key life value') -@click.option('--phase2-auth', - '--p2-auth', - default=None, - type=click.Choice(['MD5', 'SHA1', 'SHA256']), - help='Phase 2 authentication value') -@click.option('--phase2-crypto', - '--p2-crypto', - default=None, - type=click.Choice(['DES', '3DES', 'AES128', 'AES192', 'AES256']), - help='Phase 2 encryption value') -@click.option('--phase2-dh', - '--p2-dh', - default=None, - type=click.Choice(['0', '1', '2', '5']), - help='Phase 2 diffie hellman group value') -@click.option('--phase2-forward-secrecy', - '--p2-forward-secrecy', - default=None, - type=click.IntRange(0, 1), - help='Phase 2 perfect forward secrecy value') -@click.option('--phase2-key-ttl', - '--p2-key-ttl', - default=None, - type=click.IntRange(120, 172800), - help='Phase 2 key life value') -@environment.pass_env -def cli(env, context_id, friendly_name, remote_peer, preshared_key, - phase1_auth, phase1_crypto, phase1_dh, phase1_key_ttl, phase2_auth, - phase2_crypto, phase2_dh, phase2_forward_secrecy, phase2_key_ttl): - """Update tunnel context properties. - - Updates are made atomically, so either all are accepted or none are. - - Key life values must be in the range 120-172800. - - Phase 2 perfect forward secrecy must be in the range 0-1. - - A separate configuration request should be made to realize changes on - network devices. - """ - manager = SoftLayer.IPSECManager(env.client) - succeeded = manager.update_tunnel_context( - context_id, - friendly_name=friendly_name, - remote_peer=remote_peer, - preshared_key=preshared_key, - phase1_auth=phase1_auth, - phase1_crypto=phase1_crypto, - phase1_dh=phase1_dh, - phase1_key_ttl=phase1_key_ttl, - phase2_auth=phase2_auth, - phase2_crypto=phase2_crypto, - phase2_dh=phase2_dh, - phase2_forward_secrecy=phase2_forward_secrecy, - phase2_key_ttl=phase2_key_ttl - ) - if succeeded: - env.out(f'Updated context #{context_id}') - else: - raise CLIHalt(f'Failed to update context #{context_id}') diff --git a/SoftLayer/managers/ipsec.py b/SoftLayer/managers/ipsec.py deleted file mode 100644 index 0212d7732..000000000 --- a/SoftLayer/managers/ipsec.py +++ /dev/null @@ -1,317 +0,0 @@ -""" - SoftLayer.ipsec - ~~~~~~~~~~~~~~~~~~ - IPSec VPN Manager - - :license: MIT, see LICENSE for more details. -""" - -from SoftLayer.exceptions import SoftLayerAPIError -from SoftLayer.managers import ordering -from SoftLayer import utils - - -class IPSECManager(utils.IdentifierMixin, object): - """Manage SoftLayer IPSEC VPN tunnel contexts. - - This provides helpers to manage IPSEC contexts, private and remote subnets, - and NAT translations. - - :param SoftLayer.API.BaseClient client: the client instance - :param SoftLayer.API.BaseClient account: account service client - :param SoftLayer.API.BaseClient context: tunnel context client - :param SoftLayer.API.BaseClient customer_subnet: remote subnet client - """ - - def __init__(self, client): - self.client = client - self.account = client['Account'] - self.context = client['Network_Tunnel_Module_Context'] - self.remote_subnet = client['Network_Customer_Subnet'] - - def add_internal_subnet(self, context_id, subnet_id): - """Add an internal subnet to a tunnel context. - - :param int context_id: The id-value representing the context instance. - :param int subnet_id: The id-value representing the internal subnet. - :return bool: True if internal subnet addition was successful. - """ - return self.context.addPrivateSubnetToNetworkTunnel(subnet_id, - id=context_id) - - def add_remote_subnet(self, context_id, subnet_id): - """Adds a remote subnet to a tunnel context. - - :param int context_id: The id-value representing the context instance. - :param int subnet_id: The id-value representing the remote subnet. - :return bool: True if remote subnet addition was successful. - """ - return self.context.addCustomerSubnetToNetworkTunnel(subnet_id, - id=context_id) - - def add_service_subnet(self, context_id, subnet_id): - """Adds a service subnet to a tunnel context. - - :param int context_id: The id-value representing the context instance. - :param int subnet_id: The id-value representing the service subnet. - :return bool: True if service subnet addition was successful. - """ - return self.context.addServiceSubnetToNetworkTunnel(subnet_id, - id=context_id) - - def apply_configuration(self, context_id): - """Requests network configuration for a tunnel context. - - :param int context_id: The id-value representing the context instance. - :return bool: True if the configuration request was successfully queued. - """ - return self.context.applyConfigurationsToDevice(id=context_id) - - def create_remote_subnet(self, account_id, identifier, cidr): - """Creates a remote subnet on the given account. - - :param string account_id: The account identifier. - :param string identifier: The network identifier of the remote subnet. - :param string cidr: The CIDR value of the remote subnet. - :return dict: Mapping of properties for the new remote subnet. - """ - return self.remote_subnet.createObject({ - 'accountId': account_id, - 'cidr': cidr, - 'networkIdentifier': identifier - }) - - def create_translation(self, context_id, static_ip, remote_ip, notes): - """Creates an address translation on a tunnel context/ - - :param int context_id: The id-value representing the context instance. - :param string static_ip: The IP address value representing the - internal side of the translation entry, - :param string remote_ip: The IP address value representing the remote - side of the translation entry, - :param string notes: The notes to supply with the translation entry, - :return dict: Mapping of properties for the new translation entry. - """ - return self.context.createAddressTranslation({ - 'customerIpAddress': remote_ip, - 'internalIpAddress': static_ip, - 'notes': notes - }, id=context_id) - - def delete_remote_subnet(self, subnet_id): - """Deletes a remote subnet from the current account. - - :param string subnet_id: The id-value representing the remote subnet. - :return bool: True if subnet deletion was successful. - """ - return self.remote_subnet.deleteObject(id=subnet_id) - - def get_tunnel_context(self, context_id, **kwargs): - """Retrieves the network tunnel context instance. - - :param int context_id: The id-value representing the context instance. - :return dict: Mapping of properties for the tunnel context. - :raise SoftLayerAPIError: If a context cannot be found. - """ - _filter = utils.NestedDict(kwargs.get('filter') or {}) - _filter['networkTunnelContexts']['id'] = utils.query_filter(context_id) - - kwargs['filter'] = _filter.to_dict() - contexts = self.account.getNetworkTunnelContexts(**kwargs) - if len(contexts) == 0: - raise SoftLayerAPIError('SoftLayer_Exception_ObjectNotFound', - f'Unable to find object with id of \'{context_id}\'') - return contexts[0] - - def get_translation(self, context_id, translation_id): - """Retrieves a translation entry for the given id values. - - :param int context_id: The id-value representing the context instance. - :param int translation_id: The id-value representing the translation - instance. - :return dict: Mapping of properties for the translation entry. - :raise SoftLayerAPIError: If a translation cannot be found. - """ - translation = next((x for x in self.get_translations(context_id) - if x['id'] == translation_id), None) - if translation is None: - raise SoftLayerAPIError('SoftLayer_Exception_ObjectNotFound', - f'Unable to find object with id of \'{translation_id}\'') - return translation - - def get_translations(self, context_id): - """Retrieves all translation entries for a tunnel context. - - :param int context_id: The id-value representing the context instance. - :return list(dict): Translations associated with the given context - """ - _mask = ('[mask[addressTranslations[customerIpAddressRecord,' - 'internalIpAddressRecord]]]') - context = self.get_tunnel_context(context_id, mask=_mask) - # Pull the internal and remote IP addresses into the translation - for translation in context.get('addressTranslations', []): - remote_ip = translation.get('customerIpAddressRecord', {}) - internal_ip = translation.get('internalIpAddressRecord', {}) - translation['customerIpAddress'] = remote_ip.get('ipAddress', '') - translation['internalIpAddress'] = internal_ip.get('ipAddress', '') - translation.pop('customerIpAddressRecord', None) - translation.pop('internalIpAddressRecord', None) - return context['addressTranslations'] - - def get_tunnel_contexts(self, **kwargs): - """Retrieves network tunnel module context instances. - - :return list(dict): Contexts associated with the current account. - """ - return self.account.getNetworkTunnelContexts(**kwargs) - - def remove_internal_subnet(self, context_id, subnet_id): - """Remove an internal subnet from a tunnel context. - - :param int context_id: The id-value representing the context instance. - :param int subnet_id: The id-value representing the internal subnet. - :return bool: True if internal subnet removal was successful. - """ - return self.context.removePrivateSubnetFromNetworkTunnel(subnet_id, - id=context_id) - - def remove_remote_subnet(self, context_id, subnet_id): - """Removes a remote subnet from a tunnel context. - - :param int context_id: The id-value representing the context instance. - :param int subnet_id: The id-value representing the remote subnet. - :return bool: True if remote subnet removal was successful. - """ - return self.context.removeCustomerSubnetFromNetworkTunnel(subnet_id, - id=context_id) - - def remove_service_subnet(self, context_id, subnet_id): - """Removes a service subnet from a tunnel context. - - :param int context_id: The id-value representing the context instance. - :param int subnet_id: The id-value representing the service subnet. - :return bool: True if service subnet removal was successful. - """ - return self.context.removeServiceSubnetFromNetworkTunnel(subnet_id, - id=context_id) - - def remove_translation(self, context_id, translation_id): - """Removes a translation entry from a tunnel context. - - :param int context_id: The id-value representing the context instance. - :param int translation_id: The id-value representing the translation. - :return bool: True if translation entry removal was successful. - """ - return self.context.deleteAddressTranslation(translation_id, - id=context_id) - - def update_translation(self, context_id, translation_id, static_ip=None, - remote_ip=None, notes=None): - """Updates an address translation entry using the given values. - - :param int context_id: The id-value representing the context instance. - :param dict template: A key-value mapping of translation properties. - :param string static_ip: The static IP address value to update. - :param string remote_ip: The remote IP address value to update. - :param string notes: The notes value to update. - :return bool: True if the update was successful. - """ - translation = self.get_translation(context_id, translation_id) - - if static_ip is not None: - translation['internalIpAddress'] = static_ip - translation.pop('internalIpAddressId', None) - if remote_ip is not None: - translation['customerIpAddress'] = remote_ip - translation.pop('customerIpAddressId', None) - if notes is not None: - translation['notes'] = notes - self.context.editAddressTranslation(translation, id=context_id) - return True - - def update_tunnel_context(self, context_id, friendly_name=None, - remote_peer=None, preshared_key=None, - phase1_auth=None, phase1_crypto=None, - phase1_dh=None, phase1_key_ttl=None, - phase2_auth=None, phase2_crypto=None, - phase2_dh=None, phase2_forward_secrecy=None, - phase2_key_ttl=None): - """Updates a tunnel context using the given values. - - :param string context_id: The id-value representing the context. - :param string friendly_name: The friendly name value to update. - :param string remote_peer: The remote peer IP address value to update. - :param string preshared_key: The preshared key value to update. - :param string phase1_auth: The phase 1 authentication value to update. - :param string phase1_crypto: The phase 1 encryption value to update. - :param string phase1_dh: The phase 1 diffie hellman group value - to update. - :param string phase1_key_ttl: The phase 1 key life value to update. - :param string phase2_auth: The phase 2 authentication value to update. - :param string phase2_crypto: The phase 2 encryption value to update. - :param string phase2_df: The phase 2 diffie hellman group value - to update. - :param string phase2_forward_secriecy: The phase 2 perfect forward - secrecy value to update. - :param string phase2_key_ttl: The phase 2 key life value to update. - :return bool: True if the update was successful. - """ - context = self.get_tunnel_context(context_id) - - if friendly_name is not None: - context['friendlyName'] = friendly_name - if remote_peer is not None: - context['customerPeerIpAddress'] = remote_peer - if preshared_key is not None: - context['presharedKey'] = preshared_key - if phase1_auth is not None: - context['phaseOneAuthentication'] = phase1_auth - if phase1_crypto is not None: - context['phaseOneEncryption'] = phase1_crypto - if phase1_dh is not None: - context['phaseOneDiffieHellmanGroup'] = phase1_dh - if phase1_key_ttl is not None: - context['phaseOneKeylife'] = phase1_key_ttl - if phase2_auth is not None: - context['phaseTwoAuthentication'] = phase2_auth - if phase2_crypto is not None: - context['phaseTwoEncryption'] = phase2_crypto - if phase2_dh is not None: - context['phaseTwoDiffieHellmanGroup'] = phase2_dh - if phase2_forward_secrecy is not None: - context['phaseTwoPerfectForwardSecrecy'] = phase2_forward_secrecy - if phase2_key_ttl is not None: - context['phaseTwoKeylife'] = phase2_key_ttl - return self.context.editObject(context, id=context_id) - - def order(self, datacenter, item_package): - """Create a ipsec. - - :param string datacenter: the datacenter shortname - :param string[] item_package: items array - """ - complex_type = 'SoftLayer_Container_Product_Order_Network_Tunnel_Ipsec' - ordering_manager = ordering.OrderingManager(self.client) - return ordering_manager.place_order(package_keyname='ADDITIONAL_PRODUCTS', - location=datacenter, - item_keynames=item_package, - complex_type=complex_type, - hourly=False) - - def cancel_item(self, identifier, immediate, reason): - """Cancels the specified billing item Ipsec. - - Example:: - - # Cancels ipsec id 1234 - result = mgr.cancel_item(billing_item_id=1234) - - :param int billing_id: The ID of the billing item to be cancelled. - :param string reason: The reason code for the cancellation. This should come from - :func:`get_cancellation_reasons`. - :param bool immediate: If set to True, will automatically update the cancelation ticket to request - the resource be reclaimed asap. This request still has to be reviewed by a human - :returns: True on success or an exception - """ - return self.client.call('SoftLayer_Billing_Item', 'cancelItem', - True, immediate, reason, id=identifier) diff --git a/tests/CLI/modules/ipsec_tests.py b/tests/CLI/modules/ipsec_tests.py deleted file mode 100644 index eb473101d..000000000 --- a/tests/CLI/modules/ipsec_tests.py +++ /dev/null @@ -1,559 +0,0 @@ -""" - SoftLayer.tests.CLI.modules.ipsec_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - :license: MIT, see LICENSE for more details. -""" - -import json - -from SoftLayer.CLI.exceptions import ArgumentError -from SoftLayer.CLI.exceptions import CLIHalt -from SoftLayer.fixtures import SoftLayer_Product_Order -from SoftLayer.fixtures import SoftLayer_Product_Package -from SoftLayer import testing -from SoftLayer import utils - - -class IPSECTests(testing.TestCase): - - def test_ipsec_configure(self): - mock_account = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - mock_account.return_value = [{'id': 445}] - - mock_tunnel = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'applyConfigurationsToDevice') - mock_tunnel.return_value = True - - result = self.run_command(['ipsec', 'configure', '445']) - self.assert_no_fail(result) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'applyConfigurationsToDevice', - identifier=445) - self.assertEqual('Configuration request received for context #445\n', - result.output) - - def test_ipsec_configure_fails(self): - mock_account = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - mock_account.return_value = [{'id': 445}] - - mock_tunnel = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'applyConfigurationsToDevice') - mock_tunnel.return_value = False - - result = self.run_command(['ipsec', 'configure', '445']) - self.assertIsInstance(result.exception, CLIHalt) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'applyConfigurationsToDevice', - identifier=445) - self.assertEqual(('Failed to enqueue configuration request for ' - 'context #445\n'), - result.output) - - def test_ipsec_detail(self): - _mask = ('[mask[id,accountId,advancedConfigurationFlag,createDate,' - 'customerPeerIpAddress,modifyDate,name,friendlyName,' - 'internalPeerIpAddress,phaseOneAuthentication,' - 'phaseOneDiffieHellmanGroup,phaseOneEncryption,' - 'phaseOneKeylife,phaseTwoAuthentication,' - 'phaseTwoDiffieHellmanGroup,phaseTwoEncryption,' - 'phaseTwoKeylife,phaseTwoPerfectForwardSecrecy,presharedKey,' - 'addressTranslations[internalIpAddressRecord[ipAddress],' - 'customerIpAddressRecord[ipAddress]],internalSubnets,' - 'customerSubnets,staticRouteSubnets,serviceSubnets]]') - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{ - 'id': 445, - 'name': 'der tunnel', - 'friendlyName': 'the tunnel', - 'internalPeerIpAddress': '10.0.0.1', - 'customerPeerIpAddress': '50.0.0.1', - 'advancedConfigurationFlag': 0, - 'presharedKey': 'secret', - 'phaseOneAuthentication': 'MD5', - 'phaseOneDiffieHellmanGroup': 1, - 'phaseOneEncryption': 'DES', - 'phaseOneKeylife': 600, - 'phaseTwoAuthentication': 'MD5', - 'phaseTwoDiffieHellmanGroup': 1, - 'phaseTwoEncryption': 'DES', - 'phaseTwoKeylife': 600, - 'phaseTwoPerfectForwardSecrecy': 0, - 'createDate': '2017-05-17T12:00:00-06:00', - 'modifyDate': '2017-05-17T12:01:00-06:00', - 'addressTranslations': [{ - 'id': 872341, - 'internalIpAddressId': 982341, - 'internalIpAddressRecord': {'ipAddress': '10.0.0.1'}, - 'customerIpAddressId': 872422, - 'customerIpAddressRecord': {'ipAddress': '50.0.0.1'}, - 'notes': 'surprise!' - }], - 'internalSubnets': [{ - 'id': 324113, - 'networkIdentifier': '10.20.0.0', - 'cidr': 29, - 'note': 'Private Network' - }], - 'customerSubnets': [{ - 'id': 873411, - 'networkIdentifier': '50.0.0.0', - 'cidr': 26, - 'note': 'Offsite Network' - }], - 'serviceSubnets': [{ - 'id': 565312, - 'networkIdentifier': '100.10.0.0', - 'cidr': 26, - 'note': 'Service Network' - }], - 'staticRouteSubnets': [{ - 'id': 998232, - 'networkIdentifier': '50.50.0.0', - 'cidr': 29, - 'note': 'Static Network' - }] - }] - result = self.run_command(['ipsec', 'detail', '445', '-iat', '-iis', '-irs', '-isr', '-iss']) - - split_output = [] - # Converts Rich JSON output to actual JSON data. JSON UTIL - for table in utils.decode_stacked(result.output): - split_output.append(table) - - self.assertEqual(6, len(split_output)) - self.assert_no_fail(result) - self.assert_called_with('SoftLayer_Account', 'getNetworkTunnelContexts', mask=_mask) - self.assertEqual({'id': 445, - 'name': 'der tunnel', - 'friendly name': 'the tunnel', - 'internal peer IP address': '10.0.0.1', - 'remote peer IP address': '50.0.0.1', - 'advanced configuration flag': 0, - 'preshared key': 'secret', - 'phase 1 authentication': 'MD5', - 'phase 1 diffie hellman group': 1, - 'phase 1 encryption': 'DES', - 'phase 1 key life': 600, - 'phase 2 authentication': 'MD5', - 'phase 2 diffie hellman group': 1, - 'phase 2 encryption': 'DES', - 'phase 2 key life': 600, - 'phase 2 perfect forward secrecy': 0, - 'created': '2017-05-17T12:00:00-06:00', - 'modified': '2017-05-17T12:01:00-06:00'}, - split_output[0]) - self.assertEqual([{'id': 872341, - 'remote IP address': '50.0.0.1', - 'remote IP address id': 872422, - 'static IP address': '10.0.0.1', - 'static IP address id': 982341, - 'note': 'surprise!'}], - split_output[1]) - self.assertEqual([{'id': 324113, - 'network identifier': '10.20.0.0', - 'cidr': 29, - 'note': 'Private Network'}], - split_output[2]) - self.assertEqual([{'id': 873411, - 'network identifier': '50.0.0.0', - 'cidr': 26, - 'note': 'Offsite Network'}], - split_output[3]) - self.assertEqual([{'id': 998232, - 'network identifier': '50.50.0.0', - 'cidr': 29, - 'note': 'Static Network'}], - split_output[4]) - self.assertEqual([{'id': 565312, - 'network identifier': '100.10.0.0', - 'cidr': 26, - 'note': 'Service Network'}], - split_output[5]) - - def test_ipsec_list(self): - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{'id': 445, - 'name': 'der tunnel', - 'friendlyName': 'the tunnel', - 'internalPeerIpAddress': '10.0.0.1', - 'customerPeerIpAddress': '50.0.0.1', - 'advancedConfigurationFlag': 0, - 'presharedKey': 'secret', - 'phaseOneAuthentication': 'MD5', - 'phaseOneDiffieHellmanGroup': 1, - 'phaseOneEncryption': 'DES', - 'phaseOneKeylife': 600, - 'phaseTwoAuthentication': 'MD5', - 'phaseTwoDiffieHellmanGroup': 1, - 'phaseTwoEncryption': 'DES', - 'phaseTwoKeylife': 600, - 'phaseTwoPerfectForwardSecrecy': 0, - 'createDate': '2017-05-17T12:00:00-06:00', - 'modifyDate': '2017-05-17T12:01:00-06:00'}] - result = self.run_command(['ipsec', 'list']) - - self.assert_no_fail(result) - self.assertEqual([{ - 'id': 445, - 'name': 'der tunnel', - 'friendly name': 'the tunnel', - 'internal peer IP address': '10.0.0.1', - 'remote peer IP address': '50.0.0.1', - 'created': '2017-05-17T12:00:00-06:00' - }], json.loads(result.output)) - - def test_ipsec_update(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445, - 'name': 'der tunnel', - 'friendlyName': 'the tunnel', - 'internalPeerIpAddress': '10.0.0.1', - 'customerPeerIpAddress': '50.0.0.1', - 'advancedConfigurationFlag': 0, - 'presharedKey': 'secret', - 'phaseOneAuthentication': 'MD5', - 'phaseOneDiffieHellmanGroup': 1, - 'phaseOneEncryption': 'DES', - 'phaseOneKeylife': 600, - 'phaseTwoAuthentication': 'MD5', - 'phaseTwoDiffieHellmanGroup': 1, - 'phaseTwoEncryption': 'DES', - 'phaseTwoKeylife': 600, - 'phaseTwoPerfectForwardSecrecy': 0}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'editObject') - tunnel_mock.return_value = True - - result = self.run_command(['ipsec', 'update', '445', - '--friendly-name=ipsec tunnel', - '--remote-peer=50.0.0.2', - '--preshared-key=enigma', - '--p1-auth=SHA256', '--p1-crypto=AES256', - '--p1-dh=5', '--p1-key-ttl=120', - '--p2-auth=SHA1', '--p2-crypto=AES192', - '--p2-dh=2', '--p2-forward-secrecy=1', - '--p2-key-ttl=240']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Updated context #445\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'editObject', - identifier=445, - args=({'id': 445, - 'name': 'der tunnel', - 'friendlyName': 'ipsec tunnel', - 'internalPeerIpAddress': '10.0.0.1', - 'customerPeerIpAddress': '50.0.0.2', - 'advancedConfigurationFlag': 0, - 'presharedKey': 'enigma', - 'phaseOneAuthentication': 'SHA256', - 'phaseOneDiffieHellmanGroup': '5', - 'phaseOneEncryption': 'AES256', - 'phaseOneKeylife': 120, - 'phaseTwoAuthentication': 'SHA1', - 'phaseTwoDiffieHellmanGroup': '2', - 'phaseTwoEncryption': 'AES192', - 'phaseTwoKeylife': 240, - 'phaseTwoPerfectForwardSecrecy': 1},)) - - def test_ipsec_update_fails(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'editObject') - tunnel_mock.return_value = False - - result = self.run_command(['ipsec', 'update', '445']) - self.assertIsInstance(result.exception, CLIHalt) - self.assertEqual('Failed to update context #445\n', result.output) - - def test_ipsec_subnet_add_internal(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'addPrivateSubnetToNetworkTunnel') - tunnel_mock.return_value = True - - result = self.run_command(['ipsec', 'subnet-add', '445', '-tinternal', - '-s234716']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Added internal subnet #234716\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'addPrivateSubnetToNetworkTunnel', - identifier=445, - args=(234716,)) - - def test_ipsec_subnet_add_remote(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445, 'accountId': 999000}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'addCustomerSubnetToNetworkTunnel') - tunnel_mock.return_value = True - - subnet_mock = self.set_mock('SoftLayer_Network_Customer_Subnet', - 'createObject') - subnet_mock.return_value = {'id': 234716} - - result = self.run_command(['ipsec', 'subnet-add', '445', '-tremote', - '-n50.0.0.0/26']) - self.assert_no_fail(result) - self.assertEqual(result.output, - ('Created subnet 50.0.0.0/26 #234716\n' - 'Added remote subnet #234716\n')) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'addCustomerSubnetToNetworkTunnel', - identifier=445, - args=(234716,)) - self.assert_called_with('SoftLayer_Network_Customer_Subnet', - 'createObject', - args=({'networkIdentifier': '50.0.0.0', - 'cidr': 26, - 'accountId': 999000},)) - - def test_ipsec_subnet_add_service(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'addServiceSubnetToNetworkTunnel') - tunnel_mock.return_value = True - - result = self.run_command(['ipsec', 'subnet-add', '445', '-tservice', - '-s234716']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Added service subnet #234716\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'addServiceSubnetToNetworkTunnel', - identifier=445, - args=(234716,)) - - def test_ipsec_subnet_add_without_id_or_network(self): - result = self.run_command(['ipsec', 'subnet-add', '445', '-tinternal']) - self.assertIsInstance(result.exception, ArgumentError) - - def test_ipsec_subnet_add_internal_with_network(self): - result = self.run_command(['ipsec', 'subnet-add', '445', '-tinternal', - '-n50.0.0.0/26']) - self.assertIsInstance(result.exception, ArgumentError) - - def test_ipsec_subnet_add_fails(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'addPrivateSubnetToNetworkTunnel') - tunnel_mock.return_value = False - - result = self.run_command(['ipsec', 'subnet-add', '445', '-tinternal', - '-s234716']) - self.assertIsInstance(result.exception, CLIHalt) - self.assertEqual(result.output, - 'Failed to add internal subnet #234716\n') - - def test_ipsec_subnet_remove_internal(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'removePrivateSubnetFromNetworkTunnel') - tunnel_mock.return_value = True - - result = self.run_command(['ipsec', 'subnet-remove', '445', - '-tinternal', '-s234716']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Removed internal subnet #234716\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'removePrivateSubnetFromNetworkTunnel', - identifier=445, - args=(234716,)) - - def test_ipsec_subnet_remove_remote(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'removeCustomerSubnetFromNetworkTunnel') - tunnel_mock.return_value = True - - result = self.run_command(['ipsec', 'subnet-remove', '445', - '-tremote', '-s234716']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Removed remote subnet #234716\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'removeCustomerSubnetFromNetworkTunnel', - identifier=445, - args=(234716,)) - - def test_ipsec_subnet_remove_service(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'removeServiceSubnetFromNetworkTunnel') - tunnel_mock.return_value = True - - result = self.run_command(['ipsec', 'subnet-remove', '445', - '-tservice', '-s234716']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Removed service subnet #234716\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'removeServiceSubnetFromNetworkTunnel', - identifier=445, - args=(234716,)) - - def test_ipsec_subnet_remove_fails(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'removePrivateSubnetFromNetworkTunnel') - tunnel_mock.return_value = False - - result = self.run_command(['ipsec', 'subnet-remove', '445', - '-tinternal', '-s234716']) - self.assertIsInstance(result.exception, CLIHalt) - self.assertEqual(result.output, - 'Failed to remove internal subnet #234716\n') - - def test_ipsec_translation_add(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'createAddressTranslation') - tunnel_mock.return_value = {'id': 872843} - - result = self.run_command(['ipsec', 'translation-add', '445', - '-s10.50.0.0', '-r50.50.0.0', '-nlost']) - self.assert_no_fail(result) - self.assertEqual(result.output, - ('Created translation from 10.50.0.0 to 50.50.0.0 ' - '#872843\n')) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'createAddressTranslation', - identifier=445, - args=({'customerIpAddress': '50.50.0.0', - 'internalIpAddress': '10.50.0.0', - 'notes': 'lost'},)) - - def test_ipsec_translation_remove(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445, - 'addressTranslations': [{'id': 872843}]}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'deleteAddressTranslation') - tunnel_mock.return_value = True - - result = self.run_command(['ipsec', 'translation-remove', '445', - '-t872843']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Removed translation #872843\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'deleteAddressTranslation', - identifier=445, - args=(872843,)) - - def test_ipsec_translation_remove_fails(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445, - 'addressTranslations': [{'id': 872843}]}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'deleteAddressTranslation') - tunnel_mock.return_value = False - - result = self.run_command(['ipsec', 'translation-remove', '445', - '-t872843']) - self.assertIsInstance(result.exception, CLIHalt) - self.assertEqual(result.output, - 'Failed to remove translation #872843\n') - - def test_ipsec_translation_update(self): - account_mock = self.set_mock('SoftLayer_Account', - 'getNetworkTunnelContexts') - account_mock.return_value = [{'id': 445, - 'addressTranslations': [{'id': 872843}]}] - - tunnel_mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'editAddressTranslation') - tunnel_mock.return_value = {'id': 872843} - - result = self.run_command(['ipsec', 'translation-update', '445', - '-t872843', '-s10.50.0.1', '-r50.50.0.1', - '-nlost']) - self.assert_no_fail(result) - self.assertEqual(result.output, 'Updated translation #872843\n') - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'editAddressTranslation', - identifier=445, - args=({'id': 872843, - 'internalIpAddress': '10.50.0.1', - 'customerIpAddress': '50.50.0.1', - 'notes': 'lost'},)) - - def test_ipsec_order(self): - _mock = self.set_mock('SoftLayer_Product_Package', 'getItems') - _mock.return_value = SoftLayer_Product_Package.getItems_IPSEC - - order_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') - order_mock.return_value = SoftLayer_Product_Order.ipsec_placeOrder - result = self.run_command(['ipsec', 'order', '-d', 'dal13']) - self.assert_no_fail(result) - - def test_ipsec_cancel(self): - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{ - "createDate": "2013-11-05T16:03:53-06:00", - "id": 445, - "internalPeerIpAddress": "184.172.127.9", - "modifyDate": "2022-07-19T09:34:53-06:00", - "name": "ipsec003", - "phaseOneAuthentication": "MD5", - "phaseOneDiffieHellmanGroup": 2, - "phaseOneEncryption": "3DES", - "phaseOneKeylife": 14400, - "phaseTwoAuthentication": "MD5", - "phaseTwoDiffieHellmanGroup": 2, - "phaseTwoEncryption": "3DES", - "phaseTwoKeylife": 3600, - "phaseTwoPerfectForwardSecrecy": 1, - "billingItem": { - "allowCancellationFlag": 1, - "categoryCode": "network_tunnel", - "createDate": "2022-07-19T09:34:52-06:00", - "cycleStartDate": "2022-08-03T23:07:43-06:00", - "description": "IPSEC - Standard", - "id": 977194617, - "lastBillDate": "2022-08-03T23:07:43-06:00", - "modifyDate": "2022-08-03T23:07:43-06:00", - "nextBillDate": "2022-09-03T23:00:00-06:00", - "oneTimeFee": "0", - "orderItemId": 932515967, - "recurringMonths": 1, - "serviceProviderId": 1, - }}] - - mock = self.set_mock('SoftLayer_Billing_Item', 'cancelItem') - mock.return_value = True - result = self.run_command(['ipsec', 'cancel', '445', '--immediate', '--reason', 'test']) - self.assert_no_fail(result) diff --git a/tests/managers/ipsec_tests.py b/tests/managers/ipsec_tests.py deleted file mode 100644 index b47ad8677..000000000 --- a/tests/managers/ipsec_tests.py +++ /dev/null @@ -1,327 +0,0 @@ -""" - SoftLayer.tests.managers.ipsec_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - :license: MIT, see LICENSE for more details. -""" -from unittest.mock import MagicMock as MagicMock - -import SoftLayer -from SoftLayer.exceptions import SoftLayerAPIError -from SoftLayer.fixtures import SoftLayer_Product_Order -from SoftLayer.fixtures import SoftLayer_Product_Package - -from SoftLayer import testing - - -class IPSECTests(testing.TestCase): - - def set_up(self): - self.ipsec = SoftLayer.IPSECManager(self.client) - - def test_add_internal_subnet(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'addPrivateSubnetToNetworkTunnel') - mock.return_value = True - self.assertEqual(self.ipsec.add_internal_subnet(445, 565787), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'addPrivateSubnetToNetworkTunnel', - args=(565787,), - identifier=445) - - def test_add_remote_subnet(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'addCustomerSubnetToNetworkTunnel') - mock.return_value = True - self.assertEqual(self.ipsec.add_remote_subnet(445, 565787), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'addCustomerSubnetToNetworkTunnel', - args=(565787,), - identifier=445) - - def test_add_service_subnet(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'addServiceSubnetToNetworkTunnel') - mock.return_value = True - self.assertEqual(self.ipsec.add_service_subnet(445, 565787), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'addServiceSubnetToNetworkTunnel', - args=(565787,), - identifier=445) - - def test_apply_configuration(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'applyConfigurationsToDevice') - mock.return_value = True - self.assertEqual(self.ipsec.apply_configuration(445), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'applyConfigurationsToDevice', - args=(), - identifier=445) - - def test_create_remote_subnet(self): - mock = self.set_mock('SoftLayer_Network_Customer_Subnet', - 'createObject') - mock.return_value = {'id': 565787, - 'networkIdentifier': '50.0.0.0', - 'cidr': 29, - 'accountId': 999000} - result = self.ipsec.create_remote_subnet(999000, '50.0.0.0', 29) - self.assertEqual(result, mock.return_value) - self.assert_called_with('SoftLayer_Network_Customer_Subnet', - 'createObject', - args=({'networkIdentifier': '50.0.0.0', - 'cidr': 29, - 'accountId': 999000},)) - - def test_create_translation(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'createAddressTranslation') - mock.return_value = {'id': 787989, - 'customerIpAddress': '50.0.0.0', - 'customerIpAddressId': 672634, - 'internalIpAddress': '10.0.0.0', - 'internalIpAddressId': 871231, - 'notes': 'first translation'} - result = self.ipsec.create_translation(445, - '10.0.0.0', - '50.0.0.0', - 'first translation') - self.assertEqual(result, mock.return_value) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'createAddressTranslation', - args=({'customerIpAddress': '50.0.0.0', - 'internalIpAddress': '10.0.0.0', - 'notes': 'first translation'},), - identifier=445) - - def test_delete_remote_subnet(self): - mock = self.set_mock('SoftLayer_Network_Customer_Subnet', - 'deleteObject') - mock.return_value = True - self.assertEqual(self.ipsec.delete_remote_subnet(565787), True) - self.assert_called_with('SoftLayer_Network_Customer_Subnet', - 'deleteObject', - identifier=565787) - - def test_get_tunnel_context(self): - _filter = {'networkTunnelContexts': {'id': {'operation': 445}}} - _mask = '[mask[id]]' - - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{'id': 445}] - result = self.ipsec.get_tunnel_context(445, mask=_mask) - self.assertEqual(result, mock.return_value[0]) - self.assert_called_with('SoftLayer_Account', - 'getNetworkTunnelContexts', - filter=_filter, - mask=_mask) - - def test_get_tunnel_context_raises_error(self): - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [] - self.assertRaises(SoftLayerAPIError, - self.ipsec.get_tunnel_context, - 445) - - def test_get_translation(self): - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{'id': 445, 'addressTranslations': - [{'id': 234123}, {'id': 872341}]}] - self.assertEqual(self.ipsec.get_translation(445, 872341), - {'id': 872341, - 'customerIpAddress': '', - 'internalIpAddress': ''}) - self.assert_called_with('SoftLayer_Account', - 'getNetworkTunnelContexts') - - def test_get_translation_raises_error(self): - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{'id': 445, 'addressTranslations': - [{'id': 234123}]}] - self.assertRaises(SoftLayerAPIError, - self.ipsec.get_translation, - 445, - 872341) - - def test_get_translations(self): - _mask = ('[mask[addressTranslations[customerIpAddressRecord,' - 'internalIpAddressRecord]]]') - _filter = {'networkTunnelContexts': {'id': {'operation': 445}}} - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{'id': 445, - 'addressTranslations': [{ - 'id': 234123, - 'customerIpAddressRecord': - {'ipAddress': '50.0.0.0'}, - 'customerIpAddressId': 234112, - 'internalIpAddressRecord': - {'ipAddress': '10.0.0.0'}, - 'internalIpAddressId': 234442 - }]}] - self.assertEqual(self.ipsec.get_translations(445), - [{'id': 234123, - 'customerIpAddress': '50.0.0.0', - 'customerIpAddressId': 234112, - 'internalIpAddress': '10.0.0.0', - 'internalIpAddressId': 234442}]) - self.assert_called_with('SoftLayer_Account', - 'getNetworkTunnelContexts', - filter=_filter, - mask=_mask) - - def test_get_tunnel_contexts(self): - _mask = '[mask[addressTranslations]]' - mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') - mock.return_value = [{'id': 445}, {'id': 446}] - self.assertEqual(self.ipsec.get_tunnel_contexts(mask=_mask), - mock.return_value) - self.assert_called_with('SoftLayer_Account', - 'getNetworkTunnelContexts', - mask=_mask) - - def test_remove_internal_subnet(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'removePrivateSubnetFromNetworkTunnel') - mock.return_value = True - self.assertEqual(self.ipsec.remove_internal_subnet(445, 565787), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'removePrivateSubnetFromNetworkTunnel', - args=(565787,), - identifier=445) - - def test_remove_remote_subnet(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'removeCustomerSubnetFromNetworkTunnel') - mock.return_value = True - self.assertEqual(self.ipsec.remove_remote_subnet(445, 565787), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'removeCustomerSubnetFromNetworkTunnel', - args=(565787,), - identifier=445) - - def test_remove_service_subnet(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'removeServiceSubnetFromNetworkTunnel') - mock.return_value = True - self.assertEqual(self.ipsec.remove_service_subnet(445, 565787), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'removeServiceSubnetFromNetworkTunnel', - args=(565787,), - identifier=445) - - def test_remove_translation(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'deleteAddressTranslation') - mock.return_value = True - self.assertEqual(self.ipsec.remove_translation(445, 787547), True) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'deleteAddressTranslation', - args=(787547,), - identifier=445) - - def test_update_translation(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'editAddressTranslation') - mock.return_value = True - translation = {'id': 234123, - 'customerIpAddress': '50.0.0.0', - 'customerIpAddressId': 234112, - 'internalIpAddress': '10.0.0.0', - 'internalIpAddressId': 234442} - self.ipsec.get_translation = MagicMock(return_value=translation) - - result = self.ipsec.update_translation(445, - 234123, - static_ip='10.0.0.2', - remote_ip='50.0.0.2', - notes='do not touch') - self.assertEqual(result, True) - self.ipsec.get_translation.assert_called_with(445, 234123) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'editAddressTranslation', - args=({'id': 234123, - 'customerIpAddress': '50.0.0.2', - 'internalIpAddress': '10.0.0.2', - 'notes': 'do not touch'},), - identifier=445) - - def test_update_tunnel_context(self): - mock = self.set_mock('SoftLayer_Network_Tunnel_Module_Context', - 'editObject') - mock.return_value = True - context = {'id': 445, - 'name': 'der tunnel', - 'friendlyName': 'the tunnel', - 'internalPeerIpAddress': '10.0.0.1', - 'customerPeerIpAddress': '50.0.0.1', - 'advancedConfigurationFlag': 0, - 'presharedKey': 'secret', - 'phaseOneAuthentication': 'MD5', - 'phaseOneDiffieHellmanGroup': 1, - 'phaseOneEncryption': 'DES', - 'phaseOneKeylife': 600, - 'phaseTwoAuthentication': 'MD5', - 'phaseTwoDiffieHellmanGroup': 1, - 'phaseTwoEncryption': 'DES', - 'phaseTwoKeylife': 600, - 'phaseTwoPerfectForwardSecrecy': 0} - self.ipsec.get_tunnel_context = MagicMock(return_value=context) - - result = self.ipsec.update_tunnel_context(445, - friendly_name='ipsec tunnel', - remote_peer='50.0.0.2', - preshared_key='enigma', - phase1_auth='SHA256', - phase1_dh=5, - phase1_crypto='AES256', - phase1_key_ttl=120, - phase2_auth='SHA128', - phase2_dh=2, - phase2_crypto='AES192', - phase2_key_ttl=240, - phase2_forward_secrecy=1) - self.assertEqual(result, True) - self.ipsec.get_tunnel_context.assert_called_with(445) - self.assert_called_with('SoftLayer_Network_Tunnel_Module_Context', - 'editObject', - args=({'id': 445, - 'name': 'der tunnel', - 'friendlyName': 'ipsec tunnel', - 'internalPeerIpAddress': '10.0.0.1', - 'customerPeerIpAddress': '50.0.0.2', - 'advancedConfigurationFlag': 0, - 'presharedKey': 'enigma', - 'phaseOneAuthentication': 'SHA256', - 'phaseOneDiffieHellmanGroup': 5, - 'phaseOneEncryption': 'AES256', - 'phaseOneKeylife': 120, - 'phaseTwoAuthentication': 'SHA128', - 'phaseTwoDiffieHellmanGroup': 2, - 'phaseTwoEncryption': 'AES192', - 'phaseTwoKeylife': 240, - 'phaseTwoPerfectForwardSecrecy': 1},), - identifier=445) - - def test_order(self): - _mock = self.set_mock('SoftLayer_Product_Package', 'getItems') - _mock.return_value = SoftLayer_Product_Package.getItems_IPSEC - - _mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') - _mock.return_value = SoftLayer_Product_Order.ipsec_placeOrder - result = self.ipsec.order('dal13', ['IPSEC_STANDARD']) - order = { - 'orderDate': '2022-07-14T16:09:08-06:00', - 'orderId': 123456, 'placedOrder': {'items': [ - {'categoryCode': 'network_tunnel', - 'description': 'IPSEC - Standard', - 'id': 931479898, - 'itemId': 1092, - 'itemPriceId': '2048'}]}} - self.assertEqual(result, order) - - def test_cancel_item(self): - _mock = self.set_mock('SoftLayer_Billing_Item', 'cancelItem') - _mock.return_value = True - result = self.ipsec.cancel_item(443, True, 'test') - self.assertEqual(result, True) From 453f2355e69371b34d1cc58d6064064005bd3e0e Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 16 Jun 2025 17:20:15 -0500 Subject: [PATCH 77/77] Removed ipsec manager --- SoftLayer/managers/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index 8a6c8c095..08f7a5a73 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -17,7 +17,6 @@ from SoftLayer.managers.firewall import FirewallManager from SoftLayer.managers.hardware import HardwareManager from SoftLayer.managers.image import ImageManager -from SoftLayer.managers.ipsec import IPSECManager from SoftLayer.managers.license import LicensesManager from SoftLayer.managers.load_balancer import LoadBalancerManager from SoftLayer.managers.metadata import MetadataManager @@ -46,7 +45,6 @@ 'FirewallManager', 'HardwareManager', 'ImageManager', - 'IPSECManager', 'LicensesManager', 'LoadBalancerManager', 'MetadataManager',