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/.secrets.baseline b/.secrets.baseline index ea850e071..f0aee0650 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": "2025-06-11T21:28:32Z", "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 } @@ -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": 187, "type": "Hex High Entropy String", "verified_result": null } @@ -720,7 +720,7 @@ "hashed_secret": "9878e362285eb314cfdbaa8ee8c300c285856810", "is_secret": false, "is_verified": false, - "line_number": 323, + "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/README-internal.md b/README-internal.md index 06e0a050e..5b25abda0 100644 --- a/README-internal.md +++ b/README-internal.md @@ -11,8 +11,14 @@ 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 ``` -Then in the `~/.softlayer` config, set `verify = /etc/ssl/certs/bundleCA.pem` and that should work. +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 ## Certificate Example @@ -69,4 +75,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. diff --git a/README.rst b/README.rst index 39b0dfd4a..29536d085 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 `_. @@ -172,7 +173,6 @@ If you cannot install python 3.6+ for some reason, you will need to use a versio Python Packages --------------- -* prettytable >= 2.5.0 * click >= 8.0.4 * requests >= 2.32.2 * prompt_toolkit >= 2 diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 00dbfe74f..cff277286 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 @@ -45,7 +46,7 @@ 'raw_headers', 'limit', 'offset', - 'verify', + 'verify' )) @@ -181,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: @@ -373,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) @@ -403,6 +403,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 @@ -493,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 @@ -710,7 +711,7 @@ 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', + auth_result = self.call('SoftLayer_User_Employee', 'getEncryptedSessionToken', username, password, security_token) self.settings['softlayer']['access_token'] = auth_result['hash'] 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/CLI/cdn/cdn.py b/SoftLayer/CLI/cdn/cdn.py new file mode 100644 index 000000000..7237a126a --- /dev/null +++ b/SoftLayer/CLI/cdn/cdn.py @@ -0,0 +1,11 @@ +"""https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, deprecated=True) +def cli(): + """https://cloud.ibm.com/docs/CDN?topic=CDN-cdn-deprecation""" 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..34c50e549 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\) .*$)" ] @@ -72,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) @@ -84,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 "" @@ -103,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""" @@ -135,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/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 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/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 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/CLI/login.py b/SoftLayer/CLI/login.py index 8ee981979..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 @@ -30,16 +29,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 +50,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/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) 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/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 705b2cac6..c1dcc4b7d 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'), @@ -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/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/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/virt/migrate.py b/SoftLayer/CLI/virt/migrate.py index b4a1edef9..c1a1028bf 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.get('pendingMigrationFlag', False): + 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.get('pendingMigrationFlag', False): + 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/CLI/vlan/detail.py b/SoftLayer/CLI/vlan/detail.py index 6e16d9d8c..250461813 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, datacenter[name, longName], primaryRouter[fullyQualifiedDomainName], +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, '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..9a8df890b 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, ) @@ -44,14 +46,10 @@ 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) - mask = """mask[name, datacenterLongName, frontendRouterId, capabilities, datacenterId, backendRouterId, - backendRouterName, frontendRouterName]""" - pods = mgr.get_pods(mask=mask) + pod_mask = """mask[name, capabilities]""" + pods = mgr.get_pods(mask=pod_mask) for vlan in vlans: billing = 'Yes' if vlan.get('billingItem') else 'No' @@ -62,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'), @@ -78,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/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/auth.py b/SoftLayer/auth.py index 8698249ae..cdbbf8526 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/consts.py b/SoftLayer/consts.py index 136e2800d..25c00902f 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.7' 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/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/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/managers/__init__.py b/SoftLayer/managers/__init__.py index 214e1ce62..08f7a5a73 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 @@ -18,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 @@ -40,7 +38,6 @@ 'BandwidthManager', 'BlockStorageManager', 'CapacityManager', - 'CDNManager', 'DedicatedHostManager', 'DNSManager', 'EventLogManager', @@ -48,7 +45,6 @@ 'FirewallManager', 'HardwareManager', 'ImageManager', - 'IPSECManager', 'LicensesManager', 'LoadBalancerManager', 'MetadataManager', 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/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/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/hardware.py b/SoftLayer/managers/hardware.py index 6564fda0f..c8dea24b4 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['hardware']['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/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/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/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 49af7197f..fa8172b42 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]', @@ -496,6 +484,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'] = ( @@ -527,6 +516,12 @@ 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, datacenter[name], podName, +networkVlanFirewall[id,fullyQualifiedDomainName], attachedNetworkGateway[id,name], +firewallInterfaces, billingItem[id], tagReferences[tag[name]], +hardwareCount, subnetCount, totalPrimaryIpAddressCount, virtualGuestCount +]""" _filter = utils.NestedDict(_filter or {}) _filter['networkVlans']['id'] = utils.query_filter_orderby() @@ -541,7 +536,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', 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/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', diff --git a/SoftLayer/testing/xmlrpc.py b/SoftLayer/testing/xmlrpc.py index b60c2bf0c..e0e7e5ca2 100644 --- a/SoftLayer/testing/xmlrpc.py +++ b/SoftLayer/testing/xmlrpc.py @@ -3,6 +3,25 @@ ~~~~~~~~~~~~~~~~~~~~~~~~ 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 " \ +getInvoiceTopLevelItemsheaders \ +SoftLayer_Billing_InvoiceInitParameters \ +id1234 \ +" \ +http://127.0.0.1:4321/SoftLayer_Billing_Invoice + :license: MIT, see LICENSE for more details. """ import http.server @@ -45,22 +64,22 @@ 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") self.end_headers() + try: self.wfile.write(response_body.encode('utf-8')) except UnicodeDecodeError: @@ -70,22 +89,25 @@ 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 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')) - except Exception: + logging.exception("Error while handling request: %s", ex) + except Exception as ex: self.send_response(500) - logging.exception("Error while handling request") + logging.exception("Error while handling request: %s", ex) def log_message(self, fmt, *args): """Override log_message.""" @@ -103,7 +125,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 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..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, @@ -138,8 +140,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: @@ -164,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 ab5ebedde..e90249057 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 = {} @@ -103,13 +106,27 @@ 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 self.method == "getEncryptedSessionToken" and self.service == "SoftLayer_User_Employee": + 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 = [] + class SoftLayerListResult(list): """A SoftLayer API list result.""" @@ -121,6 +138,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..16456eda9 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: @@ -122,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)) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 9159eaaa6..5258234dd 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. @@ -494,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( @@ -513,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/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/docs/requirements.txt b/docs/requirements.txt index 2c1e4075b..ed9557af2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,6 @@ -sphinx==8.0.2 -sphinx_rtd_theme==2.0.0 +sphinx_rtd_theme==3.0.2 +sphinx==8.2.3 sphinx-click==6.0.0 click -prettytable rich diff --git a/setup.py b/setup.py index 045cb6d75..ec0f02c66 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='v6.2.5', + version='v6.2.7', description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type='text/x-rst', @@ -32,13 +32,12 @@ }, python_requires='>=3.7', install_requires=[ - 'prettytable >= 2.5.0', 'click >= 8.0.4', 'requests >= 2.32.2', 'prompt_toolkit >= 2', 'pygments >= 2.0.0', 'urllib3 >= 1.24', - 'rich == 13.7.1' + 'rich == 14.1.0' ], keywords=['softlayer', 'cloud', 'slcli', 'ibmcloud'], classifiers=[ diff --git a/snap/local/slcli.png b/snap/local/slcli.png new file mode 100644 index 000000000..5e273f36e Binary files /dev/null and b/snap/local/slcli.png differ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 4b78a2c9d..9a54221f0 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -6,11 +6,32 @@ description: | SLCLI documentation can be found here: https://softlayer-python.readthedocs.io/en/latest/ license: MIT - -base: core22 +website: https://www.ibm.com/cloud +source-code: https://github.com/softlayer/softlayer-python +issues: https://github.com/softlayer/softlayer-python/issues +contact: https://github.com/softlayer/softlayer-python +icon: snap/local/slcli.png +base: core24 grade: stable confinement: strict +platforms: + amd64: + build-on: [amd64] + build-for: [amd64] + arm64: + build-on: [arm64] + build-for: [arm64] + armhf: + build-on: [armhf] + build-for: [armhf] + ppc64el: + build-on: [ppc64el] + build-for: [ppc64el] + s390x: + build-on: [s390x] + build-for: [s390x] + apps: slcli: command: bin/slcli @@ -25,10 +46,10 @@ parts: slcli: source: https://github.com/softlayer/softlayer-python source-type: git - plugin: python + plugin: python override-pull: | - snapcraftctl pull - snapcraftctl set-version "$(git describe --tags | sed 's/^v//')" + craftctl default + craftctl set version="$(git describe --tags | sed 's/^v//')" build-packages: - python3 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): 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/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) 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/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..1532da60c --- /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['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) + + 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['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) + + 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['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) 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/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'} 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',)) 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/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/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/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/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/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') 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/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) 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/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 6fa6599e8..6e4dd01a8 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) @@ -72,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'} + }] } ] } @@ -98,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'} + }] } ] } 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') diff --git a/tests/transports/rest_tests.py b/tests/transports/rest_tests.py index 2c3d5f68f..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 @@ -37,6 +36,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, @@ -99,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/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..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 @@ -409,6 +396,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') diff --git a/tests/utils_tests.py b/tests/utils_tests.py new file mode 100644 index 000000000..2ee8474ca --- /dev/null +++ b/tests/utils_tests.py @@ -0,0 +1,84 @@ +""" + 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") + 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) + 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") + 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) + # 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) diff --git a/tools/requirements.txt b/tools/requirements.txt index 68582aec9..9c988cdca 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,9 +1,9 @@ -prettytable >= 2.5.0 + click >= 8.0.4 requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 -rich == 13.7.1 +rich == 14.1.0 # only used for soap transport # softlayer-zeep >= 5.0.0 diff --git a/tools/test-requirements.txt b/tools/test-requirements.txt index e40183675..4cae08234 100644 --- a/tools/test-requirements.txt +++ b/tools/test-requirements.txt @@ -4,7 +4,6 @@ pytest pytest-cov mock sphinx -prettytable >= 2.5.0 click >= 8.0.4 requests >= 2.32.2 prompt_toolkit >= 2 diff --git a/tox.ini b/tox.ini index 63e8ac7fc..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 \ @@ -52,6 +53,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 \