diff --git a/.gitattributes b/.gitattributes index 05041c45f..78f5fd340 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto +*.* text eol=lf # Language aware diff headers # https://tekin.co.uk/2020/10/better-git-diff-output-for-ruby-python-elixir-and-more diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index adc5408b6..d43890b44 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -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 @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 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/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b0793507..35ae72725 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7,3.8,3.9,'3.10',3.11] + python-version: [3.8,3.9,'3.10',3.11,3.12] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -29,9 +29,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -43,12 +43,28 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r tools/test-requirements.txt - name: Tox Analysis run: tox -e analysis + detectsecrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python/@v5 + with: + python-version: 3.11 + - name: Install Detect Secrets + run: | + python -m pip install --upgrade pip + pip install --upgrade "git+https://github.com/ibm/detect-secrets.git@master#egg=detect-secrets" + - name: Detect Secrets + run: | + detect-secrets scan --update .secrets.baseline + detect-secrets audit .secrets.baseline --report --fail-on-unaudited --omit-instructions \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7fc2c1ecb..dcede9b96 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: # You are encouraged to use static refs such as tags, instead of branch name # # Running "pre-commit autoupdate" automatically updates rev to latest tag - rev: 0.13.1+ibm.61.dss + rev: 0.13.1+ibm.62.dss hooks: - id: detect-secrets # pragma: whitelist secret # Add options for detect-secrets-hook binary. You can run `detect-secrets-hook --help` to list out all possible options. diff --git a/.secrets.baseline b/.secrets.baseline index c30275090..394061815 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2023-10-13T20:28:05Z", + "generated_at": "2025-02-14T20:05:29Z", "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 } @@ -532,6 +532,7 @@ "tests/CLI/modules/hardware/hardware_basic_tests.py": [ { "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", + "is_secret": false, "is_verified": false, "line_number": 57, "type": "Secret Keyword", @@ -553,7 +554,7 @@ "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", "is_secret": false, "is_verified": false, - "line_number": 76, + "line_number": 81, "type": "Secret Keyword", "verified_result": null } @@ -573,7 +574,7 @@ "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", "is_secret": false, "is_verified": false, - "line_number": 33, + "line_number": 31, "type": "Secret Keyword", "verified_result": null } @@ -583,7 +584,7 @@ "hashed_secret": "f7a9e24777ec23212c54d7a350bc5bea5477fdbb", "is_secret": false, "is_verified": false, - "line_number": 1088, + "line_number": 1077, "type": "Secret Keyword", "verified_result": null } @@ -603,7 +604,7 @@ "hashed_secret": "8de91b1f4c8ca32302ae101da16fb88fb127582a", "is_secret": false, "is_verified": false, - "line_number": 165, + "line_number": 168, "type": "Secret Keyword", "verified_result": null }, @@ -611,7 +612,7 @@ "hashed_secret": "2da422d13be8072a8dcae1e46b36add9cb2372fa", "is_secret": false, "is_verified": false, - "line_number": 190, + "line_number": 193, "type": "Secret Keyword", "verified_result": null } @@ -639,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 }, @@ -647,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 }, @@ -655,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 }, @@ -663,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 }, @@ -671,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 } @@ -699,7 +700,7 @@ "hashed_secret": "8af1f8146d96a3cd862281442d0d6c5cb6f8f9e5", "is_secret": false, "is_verified": false, - "line_number": 176, + "line_number": 181, "type": "Hex High Entropy String", "verified_result": null } @@ -719,7 +720,7 @@ "hashed_secret": "9878e362285eb314cfdbaa8ee8c300c285856810", "is_secret": false, "is_verified": false, - "line_number": 323, + "line_number": 313, "type": "Secret Keyword", "verified_result": null } @@ -747,7 +748,7 @@ "hashed_secret": "f08c5dc4980df3c1237e88b872a2429dac6be328", "is_secret": false, "is_verified": false, - "line_number": 310, + "line_number": 297, "type": "Secret Keyword", "verified_result": null }, @@ -755,13 +756,13 @@ "hashed_secret": "7e6a3680012346b94b54731e13d8a9ffa3790645", "is_secret": false, "is_verified": false, - "line_number": 396, + "line_number": 383, "type": "Secret Keyword", "verified_result": null } ] }, - "version": "0.13.1+ibm.61.dss", + "version": "0.13.1+ibm.62.dss", "word_list": { "file": null, "hash": null diff --git a/README-internal.md b/README-internal.md new file mode 100644 index 000000000..5b25abda0 --- /dev/null +++ b/README-internal.md @@ -0,0 +1,78 @@ +This document is for internal users wanting to use this library to interact with the internal API. It will not work for `api.softlayer.com`. + +## SSL: CERTIFICATE_VERIFY_FAILED fix +You need to specify the server certificate to verify the connection to the internal API since its a self signed certificate. Python's request module doesn't use the system SSL cert for some reason, so even if you can use `curl` without SSL errors becuase you installed the certificate on your system, you still need to tell python about it. Further reading: + - https://hackernoon.com/solving-the-dreadful-certificate-issues-in-python-requests-module + - https://levelup.gitconnected.com/using-custom-ca-in-python-here-is-the-how-to-for-k8s-implementations-c450451b6019 + +On Mac, after installing the softlayer.local certificate, the following worked for me: + +```bash +security export -t certs -f pemseq -k /System/Library/Keychains/SystemRootCertificates.keychain -o bundleCA.pem +sudo cp bundleCA.pem /etc/ssl/certs/bundleCA.pem +``` +Alternatively +```bash +API_HOST= +echo quit | openssl s_client -showcerts -servername "${API_HOST}" -connect "${API_HOST}":443 > cacert.pem +``` + +Then in the `~/.softlayer` config, set `verify = /etc/ssl/certs/bundleCA.pem` and that should work. +You may also need to set `REQUESTS_CA_BUNDLE` -> `export REQUESTS_CA_BUNDLE=/etc/ssl/certs/bundleCA.pem` to force python to load your CA bundle + +## Certificate Example + +For use with a utility certificate. In your config file (usually `~/.softlayer`), you need to set the following: + +``` +[softlayer] +endpoint_url = https:///v3/internal/rest/ +timeout = 0 +theme = dark +auth_cert = /etc/ssl/certs/my_utility_cert-dev.pem +verify = /etc/ssl/certs/allCAbundle.pem +``` + +`auth_cert`: is your utility user certificate +`server_cert`: is the CA certificate bundle to validate the internal API ssl chain. Otherwise you get self-signed ssl errors without this. + + +```python +import SoftLayer +import logging +import click + +@click.command() +def testAuthentication(): + client = SoftLayer.CertificateClient() + result = client.call('SoftLayer_Account', 'getObject', id=12345, mask="mask[id,companyName]") + print(result) + + +if __name__ == "__main__": + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + testAuthentication() +``` + +## Employee Example + +To login with your employee username, have your config look something like this + +*NOTE*: Currently logging in with the rest endpoint doesn't quite work, so use xmlrpc until I fix [this issue](https://github.ibm.com/SoftLayer/internal-softlayer-cli/issues/10) + +``` +[softlayer] +username = +endpoint_url = https:///v3/internal/xmlrpc/ +verify = /etc/ssl/certs/allCAbundle.pem +``` + +You can login and use the `slcli` with. Use the `-i` flag to make internal API calls, otherwise it will make SLDN api calls. + +```bash +slcli -i emplogin +``` + +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 c0b508914..5f82bdd62 100644 --- a/README.rst +++ b/README.rst @@ -2,22 +2,16 @@ SoftLayer API Python Client =========================== .. image:: https://github.com/softlayer/softlayer-python/workflows/Tests/badge.svg :target: https://github.com/softlayer/softlayer-python/actions?query=workflow%3ATests - .. image:: https://github.com/softlayer/softlayer-python/workflows/documentation/badge.svg :target: https://github.com/softlayer/softlayer-python/actions?query=workflow%3Adocumentation - -.. image:: https://landscape.io/github/softlayer/softlayer-python/master/landscape.svg - :target: https://landscape.io/github/softlayer/softlayer-python/master - .. image:: https://badge.fury.io/py/SoftLayer.svg :target: http://badge.fury.io/py/SoftLayer - .. image:: https://coveralls.io/repos/github/softlayer/softlayer-python/badge.svg?branch=master :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 `_. @@ -181,7 +175,7 @@ Python Packages --------------- * prettytable >= 2.5.0 * click >= 8.0.4 -* requests >= 2.20.0 +* requests >= 2.32.2 * prompt_toolkit >= 2 * pygments >= 2.0.0 * urllib3 >= 1.24 diff --git a/SECURITY.md b/SECURITY.md index 290f09332..72ec7632d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,7 @@ Version 5.7.2 is the last version that supports python2.7. | Version | Supported | | ------- | ------------------ | -| 5.9.x | :white_check_mark: | +| 6.2.x | :white_check_mark: | | 5.7.2 | :white_check_mark: | | < 5.7.2 | :x: | diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 4d2918611..cff277286 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -7,7 +7,6 @@ """ # pylint: disable=invalid-name import time -import warnings import concurrent.futures as cf import json @@ -20,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 @@ -28,11 +28,13 @@ __all__ = [ 'create_client_from_env', + 'employee_client', 'Client', 'BaseClient', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', 'IAMClient', + 'CertificateClient' ] VALID_CALL_ARGS = set(( @@ -44,7 +46,7 @@ 'raw_headers', 'limit', 'offset', - 'verify', + 'verify' )) @@ -143,13 +145,88 @@ def create_client_from_env(username=None, return BaseClient(auth=auth, transport=transport, config_file=config_file) -def Client(**kwargs): - """Get a SoftLayer API Client using environmental settings. +def employee_client(username=None, + access_token=None, + endpoint_url=None, + timeout=None, + auth=None, + config_file=None, + proxy=None, + user_agent=None, + transport=None, + verify=True): + """Creates an INTERNAL SoftLayer API client using your environment. + + Settings are loaded via keyword arguments, environemtal variables and config file. - Deprecated in favor of create_client_from_env() + :param username: your user ID + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token) + :param password: password to use for employee authentication + :param endpoint_url: the API endpoint base URL you wish to connect to. + Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. + :param proxy: proxy to be used to make API calls + :param integer timeout: timeout for API requests + :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. + Example: `BasicAuthentication` + :param config_file: A path to a configuration file used to load settings + :param user_agent: an optional User Agent to report when making API + calls if you wish to bypass the packages built in User Agent string + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) + :param bool verify: decide to verify the server's SSL/TLS cert. """ - warnings.warn("use SoftLayer.create_client_from_env() instead", - DeprecationWarning) + settings = config.get_client_settings(username=username, + api_key=None, + endpoint_url=endpoint_url, + timeout=timeout, + proxy=proxy, + verify=None, + config_file=config_file) + + url = settings.get('endpoint_url', '') + verify = settings.get('verify', True) + + if 'internal' not in url: + raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.") + + if transport is None: + if url is not None and '/rest' in url: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + else: + # Default the transport to use XMLRPC + transport = transports.XmlRpcTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + + if access_token is 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 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: + # This is for logging in mostly. + LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.") + return EmployeeClient(auth=None, transport=transport, config_file=config_file) + + +def Client(**kwargs): + """Get a SoftLayer API Client using environmental settings.""" return create_client_from_env(**kwargs) @@ -157,19 +234,30 @@ class BaseClient(object): """Base SoftLayer API client. :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase - :param transport: An object that's callable with this signature: - transport(SoftLayer.transports.Request) + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) """ - _prefix = "SoftLayer_" + auth: slauth.AuthenticationBase def __init__(self, auth=None, transport=None, config_file=None): if config_file is None: config_file = CONFIG_FILE - self.auth = auth self.config_file = config_file self.settings = config.get_config(self.config_file) + self.__setAuth(auth) + self.__setTransport(transport) + + def __setAuth(self, auth=None): + """Prepares the authentication property""" + self.auth = auth + def __setTransport(self, transport=None): + """Prepares the transport property""" + verify = self.settings['softlayer'].get('verify') + if verify == "False": + verify = False + elif verify == "True": + verify = True if transport is None: url = self.settings['softlayer'].get('endpoint_url') if url is not None and '/rest' in url: @@ -180,7 +268,7 @@ def __init__(self, auth=None, transport=None, config_file=None): # prevents an exception incase timeout is a float number. timeout=int(self.settings['softlayer'].getfloat('timeout', 0)), user_agent=consts.USER_AGENT, - verify=self.settings['softlayer'].getboolean('verify'), + verify=verify, ) else: # Default the transport to use XMLRPC @@ -189,14 +277,12 @@ def __init__(self, auth=None, transport=None, config_file=None): proxy=self.settings['softlayer'].get('proxy'), timeout=int(self.settings['softlayer'].getfloat('timeout', 0)), user_agent=consts.USER_AGENT, - verify=self.settings['softlayer'].getboolean('verify'), + verify=verify, ) self.transport = transport - def authenticate_with_password(self, username, password, - security_question_id=None, - security_question_answer=None): + def authenticate_with_password(self, username, password, security_question_id=None, security_question_answer=None): """Performs Username/Password Authentication :param string username: your SoftLayer username @@ -259,8 +345,7 @@ def call(self, service, method, *args, **kwargs): invalid_kwargs = set(kwargs.keys()) - VALID_CALL_ARGS if invalid_kwargs: - raise TypeError( - 'Invalid keyword arguments: %s' % ','.join(invalid_kwargs)) + raise TypeError('Invalid keyword arguments: %s' % ','.join(invalid_kwargs)) prefixes = (self._prefix, 'BluePages_Search', 'IntegratedOfferingTeam_Region') if self._prefix and not service.startswith(prefixes): @@ -286,9 +371,9 @@ def call(self, service, method, *args, **kwargs): request.filter = kwargs.get('filter') request.limit = kwargs.get('limit') request.offset = kwargs.get('offset') + 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) @@ -318,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 @@ -391,6 +477,31 @@ def __len__(self): return 0 +class CertificateClient(BaseClient): + """Client that works with a X509 Certificate for authentication. + + Will read the certificate file from the config file (~/.softlayer usually). + > auth_cert = /path/to/authentication/cert.pm + > server_cert = /path/to/CAcert.pem + Set auth to a SoftLayer.auth.Authentication class to manually set authentication + """ + + def __init__(self, auth=None, transport=None, config_file=None): + BaseClient.__init__(self, auth, transport, config_file) + self.__setAuth(auth) + + 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('verify', True) + auth = slauth.X509Authentication(auth_cert, serv_cert) + self.auth = auth + + def __repr__(self): + return "CertificateClient(transport=%r, auth=%r)" % (self.transport, self.auth) + + class IAMClient(BaseClient): """IBM ID Client for using IAM authentication @@ -575,6 +686,94 @@ def __repr__(self): return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth) +class EmployeeClient(BaseClient): + """Internal SoftLayer Client + + :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) + """ + + def __init__(self, auth=None, transport=None, config_file=None, account_id=None): + BaseClient.__init__(self, auth, transport, config_file) + self.account_id = account_id + + def authenticate_with_internal(self, username, password, security_token=None): + """Performs internal authentication + + :param string username: your softlayer username + :param string password: your softlayer password + :param int security_token: your 2FA token, prompt if None + """ + + self.auth = None + if security_token is None: + security_token = input("Enter your 2FA Token now: ") + if len(security_token) != 6: + raise exceptions.SoftLayerAPIError("Invalid security token: {}".format(security_token)) + + auth_result = self.call('SoftLayer_User_Employee', 'getEncryptedSessionToken', + username, password, security_token) + + self.settings['softlayer']['access_token'] = auth_result['hash'] + self.settings['softlayer']['userid'] = str(auth_result['userId']) + # self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] + + config.write_config(self.settings, self.config_file) + self.auth = slauth.EmployeeAuthentication(auth_result['userId'], auth_result['hash']) + + return auth_result + + def authenticate_with_hash(self, userId, access_token): + """Authenticates to the Internal SL API with an employee userid + token + + :param string userId: Employee UserId + :param string access_token: Employee Hash Token + """ + self.auth = slauth.EmployeeAuthentication(userId, access_token) + + def refresh_token(self, userId, auth_token): + """Refreshes the login token""" + + # Go directly to base client, to avoid infite loop if the token is super expired. + auth_result = BaseClient.call(self, 'SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId) + if len(auth_result) > 1: + for returned_data in auth_result: + # Access tokens should be 188 characters, but just incase its longer or something. + if len(returned_data) > 180: + self.settings['softlayer']['access_token'] = returned_data + else: + message = "Excepted 2 properties from refreshEncryptedToken, got {}|".format(auth_result) + raise exceptions.SoftLayerAPIError(message) + + config.write_config(self.settings, self.config_file) + self.auth = slauth.EmployeeAuthentication(userId, auth_result[0]) + return auth_result + + def call(self, service, method, *args, **kwargs): + """Handles refreshing Employee tokens in case of a HTTP 401 error""" + if (service == 'SoftLayer_Account' or service == 'Account') and not kwargs.get('id'): + if not self.account_id: + raise exceptions.SoftLayerError("SoftLayer_Account service requires an ID") + kwargs['id'] = self.account_id + + try: + return BaseClient.call(self, service, method, *args, **kwargs) + except exceptions.SoftLayerAPIError as ex: + if ex.faultCode == "SoftLayer_Exception_EncryptedToken_Expired": + userId = self.settings['softlayer'].get('userid') + access_token = self.settings['softlayer'].get('access_token') + LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) + self.refresh_token(userId, access_token) + # Try the Call again this time.... + return BaseClient.call(self, service, method, *args, **kwargs) + + else: + raise ex + + def __repr__(self): + return "EmployeeClient(transport=%r, auth=%r)" % (self.transport, self.auth) + + class Service(object): """A SoftLayer Service. 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/block/detail.py b/SoftLayer/CLI/block/detail.py index 7752e1b56..a4359fae3 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -31,77 +31,52 @@ def cli(env, volume_id): table.add_row(['Username', block_volume['username']]) table.add_row(['Type', storage_type]) table.add_row(['Capacity (GB)', capacity]) - table.add_row(['LUN Id', "%s" % block_volume['lunId']]) + table.add_row(['LUN Id', block_volume['lunId']]) if block_volume.get('provisionedIops'): - table.add_row(['IOPs', float(block_volume['provisionedIops'])]) + table.add_row(['IOPs', block_volume['provisionedIops']]) if block_volume.get('storageTierLevel'): - table.add_row([ - 'Endurance Tier', - block_volume['storageTierLevel'], - ]) - - table.add_row([ - 'Data Center', - block_volume['serviceResource']['datacenter']['name'], - ]) - table.add_row([ - 'Target IP', - block_volume['serviceResourceBackendIpAddress'], - ]) + table.add_row(['Endurance Tier', block_volume['storageTierLevel']]) + + table.add_row(['Data Center', block_volume['serviceResource']['datacenter']['name']]) + table.add_row(['Target IP', block_volume['serviceResourceBackendIpAddress']]) if block_volume['snapshotCapacityGb']: - table.add_row([ - 'Snapshot Capacity (GB)', - block_volume['snapshotCapacityGb'], - ]) + table.add_row(['Snapshot Capacity (GB)', block_volume['snapshotCapacityGb']]) if 'snapshotSizeBytes' in block_volume['parentVolume']: - table.add_row([ - 'Snapshot Used (Bytes)', - block_volume['parentVolume']['snapshotSizeBytes'], - ]) + table.add_row(['Snapshot Used (Bytes)', block_volume['parentVolume']['snapshotSizeBytes']]) - table.add_row(['# of Active Transactions', "%i" - % block_volume['activeTransactionCount']]) + table.add_row(['# of Active Transactions', block_volume['activeTransactionCount']]) if block_volume['activeTransactions']: for trans in block_volume['activeTransactions']: if 'transactionStatus' in trans and 'friendlyName' in trans['transactionStatus']: table.add_row(['Ongoing Transaction', trans['transactionStatus']['friendlyName']]) - table.add_row(['Replicant Count', "%u" % block_volume.get('replicationPartnerCount', 0)]) + table.add_row(['Replicant Count', block_volume.get('replicationPartnerCount', 0)]) if block_volume['replicationPartnerCount'] > 0: # This if/else temporarily handles a bug in which the SL API # returns a string or object for 'replicationStatus'; it seems that # the type is string for File volumes and object for Block volumes if 'message' in block_volume['replicationStatus']: - table.add_row(['Replication Status', "%s" - % block_volume['replicationStatus']['message']]) + table.add_row(['Replication Status', block_volume['replicationStatus']['message']]) else: - table.add_row(['Replication Status', "%s" - % block_volume['replicationStatus']]) + table.add_row(['Replication Status', block_volume['replicationStatus']]) + replicant_table = formatting.Table(['Id', 'Username', 'Target', 'Location', 'Schedule']) + replicant_table.align['Name'] = 'r' + replicant_table.align['Value'] = 'l' for replicant in block_volume['replicationPartners']: - replicant_table = formatting.Table(['Name', - 'Value']) - replicant_table.add_row(['Replicant Id', replicant['id']]) - replicant_table.add_row([ - 'Volume Name', - utils.lookup(replicant, 'username')]) replicant_table.add_row([ - 'Target IP', - utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) - replicant_table.add_row([ - 'Data Center', - utils.lookup(replicant, - 'serviceResource', 'datacenter', 'name')]) - replicant_table.add_row([ - 'Schedule', - utils.lookup(replicant, - 'replicationSchedule', 'type', 'keyname')]) - table.add_row(['Replicant Volumes', replicant_table]) + replicant.get('id'), + utils.lookup(replicant, 'username'), + utils.lookup(replicant, 'serviceResourceBackendIpAddress'), + utils.lookup(replicant, 'serviceResource', 'datacenter', 'name'), + utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname') + ]) + table.add_row(['Replicant Volumes', replicant_table]) if block_volume.get('originalVolumeSize'): original_volume_info = formatting.Table(['Property', 'Value']) diff --git a/SoftLayer/CLI/block/limit.py b/SoftLayer/CLI/block/limit.py index 7b64b0b98..6af71c9e3 100644 --- a/SoftLayer/CLI/block/limit.py +++ b/SoftLayer/CLI/block/limit.py @@ -23,7 +23,7 @@ def cli(env, sortby, datacenter): Example:: slcli block volume-limits This command lists the storage limits per datacenter for this account. -""" + """ block_manager = SoftLayer.BlockStorageManager(env.client) block_volumes = block_manager.list_block_volume_limit() diff --git a/SoftLayer/CLI/block/modify.py b/SoftLayer/CLI/block/modify.py index fac2af594..187ff8d7e 100644 --- a/SoftLayer/CLI/block/modify.py +++ b/SoftLayer/CLI/block/modify.py @@ -20,22 +20,21 @@ @click.option('--new-iops', '-i', type=int, help='Performance Storage IOPS, between 100 and 6000 in multiples of 100 [only for performance volumes] ' - '***If no IOPS value is specified, the original IOPS value of the volume will be used.***\n' - 'Requirements: [If original IOPS/GB for the volume is less than 0.3, new IOPS/GB must also be ' - 'less than 0.3. If original IOPS/GB for the volume is greater than or equal to 0.3, new IOPS/GB ' - 'for the volume must also be greater than or equal to 0.3.]') + '***If no IOPS value is specified, the original IOPS value of the volume will be used.***') @click.option('--new-tier', '-t', - help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] ' - '***If no tier is specified, the original tier of the volume will be used.***\n' - 'Requirements: [If original IOPS/GB for the volume is 0.25, new IOPS/GB for the volume must also ' - 'be 0.25. If original IOPS/GB for the volume is greater than 0.25, new IOPS/GB for the volume ' - 'must also be greater than 0.25.]', + help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] Classic Choices: ' + '***If no tier is specified, the original tier of the volume will be used.***', type=click.Choice(['0.25', '2', '4', '10'])) @environment.pass_env def cli(env, volume_id, new_size, new_iops, new_tier): - """Modify an existing block storage volume. + """Modify an existing block storage volume. Choices. + + Valid size and iops options can be found here: + https://cloud.ibm.com/docs/BlockStorage/index.html#provisioning-considerations + https://cloud.ibm.com/docs/BlockStorage?topic=BlockStorage-orderingBlockStorage&interface=cli Example:: + slcli block volume-modify 12345678 --new-size 1000 --new-iops 4000 This command modify a volume 12345678 with size is 1000GB, IOPS is 4000. diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py index 528e4e1f8..bdd0100e0 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -78,6 +78,7 @@ def cli(env, storage_type, size, iops, tier, os_type, 'Hourly billing is only available for the storage_as_a_service service offering' ) + order = {} if storage_type == 'performance': if iops is None: raise exceptions.CLIAbort('Option --iops required with Performance') diff --git a/SoftLayer/CLI/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 2b4b70b22..34c50e549 100644 --- a/SoftLayer/CLI/command.py +++ b/SoftLayer/CLI/command.py @@ -28,7 +28,10 @@ class OptionHighlighter(RegexHighlighter): r"(?P