diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dc02956df..d43890b44 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl 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 cf2fc9885..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,9 +173,8 @@ 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.20.0 +* requests >= 2.32.2 * prompt_toolkit >= 2 * pygments >= 2.0.0 * urllib3 >= 1.24 diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 0a2788b63..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: @@ -211,9 +212,11 @@ def employee_client(username=None, access_token = settings.get('access_token') user_id = settings.get('userid') - # Assume access_token is valid for now, user has logged in before at least. - if access_token and user_id: + if settings.get('auth_cert', False): + auth = slauth.X509Authentication(settings.get('auth_cert'), verify) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + elif access_token and user_id: auth = slauth.EmployeeAuthentication(user_id, access_token) return EmployeeClient(auth=auth, transport=transport, config_file=config_file) else: @@ -371,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) @@ -401,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 @@ -491,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 @@ -708,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/dns/zone_delete.py b/SoftLayer/CLI/dns/zone_delete.py index 83eb11273..cca4c9c9e 100644 --- a/SoftLayer/CLI/dns/zone_delete.py +++ b/SoftLayer/CLI/dns/zone_delete.py @@ -17,6 +17,7 @@ def cli(env, zone): """Delete zone. Example:: + slcli dns zone-delete ibm.com This command deletes a zone that is named ibm.com """ diff --git a/SoftLayer/CLI/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 c4c284636..2c0e324fe 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -70,7 +70,6 @@ def format_output(data, fmt='table', theme=None): # pylint: disable=R0911,R0912 return output # fallback, convert this odd object to a string - # print(f"Casting this to string {data}") return str(data) @@ -244,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 @@ -254,15 +256,17 @@ def confirm(prompt_str, default=False): def no_going_back(confirmation): """Show a confirmation to a user. - :param confirmation str: the string the user has to enter in order to - confirm their action. + :param confirmation str: the string the user has to enter in order to confirm their action. """ if not confirmation: confirmation = 'yes' 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 @@ -319,12 +323,16 @@ def __init__(self, columns, title=None, align=None): self.sortby = None self.title = title # Used to print a message if the table is empty - self.empty_message = None + self.empty_message = "-" def __bool__(self): """Useful for seeing if the table has any rows""" return len(self.rows) > 0 + def __str__(self): + """A Table should only be cast to a string if its empty""" + return self.empty_message + def set_empty_message(self, message): """Sets the empty message for this table for env.fout diff --git a/SoftLayer/CLI/globalip/assign.py b/SoftLayer/CLI/globalip/assign.py index 1e793761e..a03d83744 100644 --- a/SoftLayer/CLI/globalip/assign.py +++ b/SoftLayer/CLI/globalip/assign.py @@ -5,27 +5,51 @@ import SoftLayer from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers -target_types = {'vlan': 'SoftLayer_Network_Vlan', - 'ip': 'SoftLayer_Network_Subnet_IpAddress', - 'hardware': 'SoftLayer_Hardware_Server', - 'vsi': 'SoftLayer_Virtual_Guest'} + +# pylint: disable=unused-argument +def targetipcallback(ctx, param, value): + """This is here to allow for using --target-id in some cases. Takes the first value and returns it""" + if value: + return value[0] + return value @click.command(cls=SoftLayer.CLI.command.SLCommand, epilog="More information about types and identifiers " "on https://sldn.softlayer.com/reference/services/SoftLayer_Network_Subnet/route/") -@click.argument('identifier') +@click.argument('globalip') +@click.argument('targetip', nargs=-1, callback=targetipcallback) @click.option('--target', type=click.Choice(['vlan', 'ip', 'hardware', 'vsi']), help='choose the type. vlan, ip, hardware, vsi') -@click.option('--target-id', help='The identifier for the destination resource to route this subnet to. ') +@click.option('--target-id', help='The identifier for the destination resource to route this subnet to.') @environment.pass_env -def cli(env, identifier, target, target_id): - """Assigns the subnet to a target. +def cli(env, globalip, targetip, target, target_id): + """Assigns the GLOBALIP to TARGETIP. + GLOBALIP should be either the Global IP address, or the SoftLayer_Network_Subnet_IpAddress_Global id + See `slcli globalip list` + TARGETIP should be either the target IP address, or the SoftLayer_Network_Subnet_IpAddress id + See `slcli subnet list` Example:: + slcli globalip assign 12345678 9.111.123.456 - This command assigns IP address with ID 12345678 to a target device whose IP address is 9.111.123.456 - """ + This command assigns Global IP address with ID 12345678 to a target device whose IP address is 9.111.123.456 + slcli globalip assign 123.4.5.6 6.5.4.123 + Global IPs can be specified by their IP address + """ mgr = SoftLayer.NetworkManager(env.client) - mgr.route(identifier, target_types.get(target), target_id) + # Find SoftLayer_Network_Subnet_IpAddress_Global::id + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, globalip, name='Global IP') + + # Find Global IPs SoftLayer_Network_Subnet::id + mask = "mask[id,ipAddress[subnetId]]" + subnet = env.client.call('SoftLayer_Network_Subnet_IpAddress_Global', 'getObject', id=global_ip_id, mask=mask) + subnet_id = subnet.get('ipAddress', {}).get('subnetId') + + # For backwards compatibility + if target_id: + targetip = target_id + + mgr.route(subnet_id, 'SoftLayer_Network_Subnet_IpAddress', targetip) diff --git a/SoftLayer/CLI/globalip/cancel.py b/SoftLayer/CLI/globalip/cancel.py index 0d9394b24..920d07c71 100644 --- a/SoftLayer/CLI/globalip/cancel.py +++ b/SoftLayer/CLI/globalip/cancel.py @@ -18,12 +18,12 @@ def cli(env, identifier, force): """Cancel global IP. Example:: + slcli globalip cancel 12345 """ mgr = SoftLayer.NetworkManager(env.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, - name='global ip') + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, name='global ip') if not force: if not (env.skip_confirmations or diff --git a/SoftLayer/CLI/globalip/unassign.py b/SoftLayer/CLI/globalip/unassign.py index 563ebb106..564c74a8f 100644 --- a/SoftLayer/CLI/globalip/unassign.py +++ b/SoftLayer/CLI/globalip/unassign.py @@ -12,9 +12,21 @@ @click.argument('identifier') @environment.pass_env def cli(env, identifier): - """Unassigns a global IP from a target.""" + """Unroutes IDENTIFIER + + IDENTIFIER should be either the Global IP address, or the SoftLayer_Network_Subnet_IpAddress_Global id + Example:: + + slcli globalip unassign 123456 + + slcli globalip unassign 123.43.22.11 +""" mgr = SoftLayer.NetworkManager(env.client) - global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, - name='global ip') - mgr.unassign_global_ip(global_ip_id) + global_ip_id = helpers.resolve_id(mgr.resolve_global_ip_ids, identifier, name='global ip') + + # Find Global IPs SoftLayer_Network_Subnet::id + mask = "mask[id,ipAddress[subnetId]]" + subnet = env.client.call('SoftLayer_Network_Subnet_IpAddress_Global', 'getObject', id=global_ip_id, mask=mask) + subnet_id = subnet.get('ipAddress', {}).get('subnetId') + mgr.clear_route(subnet_id) diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py index fe8661153..404a3235f 100644 --- a/SoftLayer/CLI/hardware/detail.py +++ b/SoftLayer/CLI/hardware/detail.py @@ -173,10 +173,13 @@ def _bw_table(bw_data): def _system_table(system_data): - table = formatting.Table(['Type', 'name']) + table = formatting.Table(['Type', 'Name']) + table.align['Type'] = 'r' + table.align['Name'] = 'l' + table.sortby = 'Type' for system in system_data: - table.add_row([utils.lookup(system, 'hardwareComponentModel', - 'hardwareGenericComponentModel', - 'hardwareComponentType', 'keyName'), - utils.lookup(system, 'hardwareComponentModel', 'longDescription')]) + c_type = utils.lookup(system, 'hardwareComponentModel', 'hardwareGenericComponentModel', + 'hardwareComponentType', 'keyName') + c_name = utils.lookup(system, 'hardwareComponentModel', 'longDescription') + table.add_row([c_type, c_name]) return table 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/user/list.py b/SoftLayer/CLI/user/list.py index 17f3c8f69..779f14011 100644 --- a/SoftLayer/CLI/user/list.py +++ b/SoftLayer/CLI/user/list.py @@ -20,7 +20,8 @@ column_helper.Column('hardwareCount', ('hardwareCount',)), column_helper.Column('virtualGuestCount', ('virtualGuestCount',)), column_helper.Column('2FA', (TWO_FACTO_AUTH,)), - column_helper.Column('classicAPIKey', (CLASSIC_API_KEYS,)) + column_helper.Column('classicAPIKey', (CLASSIC_API_KEYS,)), + column_helper.Column('vpn', ('sslVpnAllowedFlag',)) ] DEFAULT_COLUMNS = [ @@ -30,6 +31,7 @@ 'displayName', '2FA', 'classicAPIKey', + 'vpn' ] @@ -48,7 +50,7 @@ def cli(env, columns): table = formatting.Table(columns.columns) for user in users: - user = _yes_format(user, [TWO_FACTO_AUTH, CLASSIC_API_KEYS]) + user = _yes_format(user, [TWO_FACTO_AUTH, CLASSIC_API_KEYS, 'sslVpnAllowedFlag']) table.add_row([value or formatting.blank() for value in columns.row(user)]) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 958c865b1..041375d86 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -17,8 +17,7 @@ @click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('identifier') -@click.option('--passwords', - is_flag=True, +@click.option('--passwords', is_flag=True, help='Show passwords (check over your shoulder!)') @click.option('--price', is_flag=True, help='Show associated prices') @environment.pass_env @@ -53,10 +52,7 @@ def cli(env, identifier, passwords=False, price=False): table.add_row(['active_transaction', formatting.active_txn(result)]) table.add_row(['datacenter', result['datacenter']['name'] or formatting.blank()]) _cli_helper_dedicated_host(env, result, table) - operating_system = utils.lookup(result, - 'operatingSystem', - 'softwareLicense', - 'softwareDescription') or {} + operating_system = utils.lookup(result, 'operatingSystem', 'softwareLicense', 'softwareDescription') or {} table.add_row(['os', operating_system.get('name', '-')]) table.add_row(['os_version', operating_system.get('version', '-')]) table.add_row(['cores', result['maxCpu']]) @@ -76,10 +72,7 @@ def cli(env, identifier, passwords=False, price=False): table.add_row(['last_transaction', last_transaction]) table.add_row(['billing', 'Hourly' if result['hourlyBillingFlag'] else 'Monthly']) - table.add_row(['preset', utils.lookup(result, 'billingItem', - 'orderItem', - 'preset', - 'keyName') or '-']) + table.add_row(['preset', utils.lookup(result, 'billingItem', 'orderItem', 'preset', 'keyName') or '-']) table.add_row(_get_owner_row(result)) table.add_row(_get_vlan_table(result)) @@ -94,9 +87,7 @@ def cli(env, identifier, passwords=False, price=False): table.add_row(['notes', result.get('notes', '-')]) if price: - total_price = utils.lookup(result, - 'billingItem', - 'nextInvoiceTotalRecurringAmount') or 0 + total_price = utils.lookup(result, 'billingItem', 'nextInvoiceTotalRecurringAmount') or 0 if total_price != 0: table.add_row(['Prices', _price_table(utils.lookup(result, 'billingItem'), total_price)]) table.add_row(['Price rate', total_price]) @@ -107,10 +98,7 @@ def cli(env, identifier, passwords=False, price=False): for component in result['softwareComponents']: for item in component['passwords']: pass_table.add_row([ - utils.lookup(component, - 'softwareLicense', - 'softwareDescription', - 'name'), + utils.lookup(component, 'softwareLicense', 'softwareDescription', 'name'), item['username'], item['password'], ]) @@ -122,10 +110,7 @@ def cli(env, identifier, passwords=False, price=False): # Test to see if this actually has a primary (public) ip address try: if not result['privateNetworkOnlyFlag']: - ptr_domains = env.client.call( - 'Virtual_Guest', 'getReverseDomainRecords', - id=vs_id, - ) + ptr_domains = env.client.call('Virtual_Guest', 'getReverseDomainRecords', id=vs_id) for ptr_domain in ptr_domains: for ptr in ptr_domain['resourceRecords']: @@ -196,8 +181,7 @@ def _get_vlan_table(result): vlan_table = formatting.Table(['type', 'number', 'id']) for vlan in result['networkVlans']: - vlan_table.add_row([ - vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) + vlan_table.add_row([vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) return ['vlans', vlan_table] diff --git a/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/config.py b/SoftLayer/config.py index e695d6b9a..f67552639 100644 --- a/SoftLayer/config.py +++ b/SoftLayer/config.py @@ -61,7 +61,8 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'proxy': '', 'userid': '', 'access_token': '', - 'verify': "True" + 'verify': "True", + 'auth_cert': '' }) config.read(config_files) @@ -74,7 +75,8 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'api_key': config.get('softlayer', 'api_key'), 'userid': config.get('softlayer', 'userid'), 'access_token': config.get('softlayer', 'access_token'), - 'verify': config.get('softlayer', 'verify') + 'verify': config.get('softlayer', 'verify'), + 'auth_cert': config.get('softlayer', 'auth_cert') } if r_config["verify"].lower() == "true": r_config["verify"] = True diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 02cc3352e..25c00902f 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v6.2.3' +VERSION = 'v6.2.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/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py b/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py index 89cd22f50..39244730b 100644 --- a/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py +++ b/SoftLayer/fixtures/SoftLayer_Network_Subnet_IpAddress_Global.py @@ -1,3 +1,3 @@ route = True unroute = True -getObject = {'id': 1234, 'billingItem': {'id': 1234}} +getObject = {'id': 1234, 'billingItem': {'id': 1234}, 'ipAddress': {'subnetId': 9988}} diff --git a/SoftLayer/managers/__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 201864606..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', @@ -841,12 +836,12 @@ def get_closed_pods(self): def route(self, subnet_id, type_serv, target): """Assigns a subnet to a specified target. - :param int subnet_id: The ID of the global IP being assigned + https://sldn.softlayer.com/reference/services/SoftLayer_Network_Subnet/route/ + :param int subnet_id: The ID of the SoftLayer_Network_Subnet_IpAddress being routed :param string type_serv: The type service to assign :param string target: The instance to assign """ - return self.client.call('SoftLayer_Network_Subnet', 'route', - type_serv, target, id=subnet_id, ) + return self.client.call('SoftLayer_Network_Subnet', 'route', type_serv, target, id=subnet_id, ) def get_datacenter(self, _filter=None, datacenter=None): """Calls SoftLayer_Location::getDatacenters() diff --git a/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/user.py b/SoftLayer/managers/user.py index 72b23dfda..87a5f0791 100644 --- a/SoftLayer/managers/user.py +++ b/SoftLayer/managers/user.py @@ -54,7 +54,7 @@ def list_users(self, objectmask=None, objectfilter=None): if objectmask is None: objectmask = """mask[id, username, displayName, userStatus[name], hardwareCount, virtualGuestCount, - email, roles, externalBindingCount,apiAuthenticationKeyCount]""" + email, roles, externalBindingCount,apiAuthenticationKeyCount, sslVpnAllowedFlag]""" return self.account_service.getUsers(mask=objectmask, filter=objectfilter) 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 36adedad3..ed9557af2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,6 @@ -sphinx==7.4.4 -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 fd008d57f..ec0f02c66 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='v6.2.3', + 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.20.0', + '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 42375ded7..9a54221f0 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -3,21 +3,38 @@ adopt-info: slcli summary: A CLI tool to interact with the SoftLayer API. description: | A command-line interface is also included and can be used to manage various SoftLayer products and services. + SLCLI documentation can be found here: https://softlayer-python.readthedocs.io/en/latest/ license: MIT - -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 -assumes: - - command-chain +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 - command-chain: - - bin/homeishome-launch environment: LC_ALL: C.UTF-8 plugs: @@ -29,18 +46,13 @@ 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 stage-packages: - - python3 - - homeishome-launch: - plugin: nil - stage-snaps: - - homeishome-launch + - python3 diff --git a/tests/CLI/formatting_table_tests.py b/tests/CLI/formatting_table_tests.py index 4d62a742b..f59764231 100644 --- a/tests/CLI/formatting_table_tests.py +++ b/tests/CLI/formatting_table_tests.py @@ -48,6 +48,26 @@ def test_key_value_table(self): result = capture.get() self.assertEqual(expected, result) + def test_key_value_table_empty(self): + + expected = """┌────────┬───────┐ +│ name │ value │ +├────────┼───────┤ +│ table2 │ - │ +└────────┴───────┘ +""" + table1 = formatting.KeyValueTable(["name", "value"]) + table2 = formatting.Table(["one", "two", "three"]) + table1.add_row(["table2", table2]) + result = formatting.format_output(table1, "table") + console = Console() + + with console.capture() as capture: + to_print = formatting.format_output(table1) + console.print(to_print) + result = capture.get() + self.assertEqual(expected, result) + def test_unrenderable_recovery_table(self): expected = """│ Sub Table │ [ 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 66abd689c..9c988cdca 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,9 +1,9 @@ -prettytable >= 2.5.0 + click >= 8.0.4 -requests >= 2.20.0 +requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 -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 6b546ccf3..4cae08234 100644 --- a/tools/test-requirements.txt +++ b/tools/test-requirements.txt @@ -4,9 +4,8 @@ pytest pytest-cov mock sphinx -prettytable >= 2.5.0 click >= 8.0.4 -requests >= 2.20.0 +requests >= 2.32.2 prompt_toolkit >= 2 pygments >= 2.0.0 urllib3 >= 1.24 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 \