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 19d4bd814..d43890b44 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,11 +39,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + 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@v1 + 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@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index d307fd937..c9eb15e54 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.14] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f307200df..83a01c0ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,6 @@ +# https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ +# Trusted Publisher stuff: https://docs.pypi.org/trusted-publishers/adding-a-publisher/ + name: Release to PyPi on: @@ -8,12 +11,17 @@ jobs: build-n-publish: name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/SoftLayer/ + permissions: + id-token: write steps: - - uses: actions/checkout@master - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Set up Python 3.14 + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.14 - name: Install pypa/build run: >- python -m @@ -28,10 +36,6 @@ jobs: --wheel --outdir dist/ . - - name: Publish đŸ“Ļ to Test PyPI + - name: đŸ“Ļ to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.CGALLO_PYPI }} - repository_url: https://upload.pypi.org/legacy/ 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/test_pypi_release.yml b/.github/workflows/test_pypi_release.yml index 12443d257..70245307b 100644 --- a/.github/workflows/test_pypi_release.yml +++ b/.github/workflows/test_pypi_release.yml @@ -1,21 +1,27 @@ # https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ +# Trusted Publisher stuff: https://docs.pypi.org/trusted-publishers/adding-a-publisher/ -name: Publish đŸ“Ļ to TestPyPI +name: TEST Publish đŸ“Ļ to TestPyPI on: push: - branches: [test-pypi ] + branches: [test-pypi] jobs: build-n-publish: - name: Build and publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI + name: TEST Build and publish Python 🐍 distributions đŸ“Ļ to PyPI and TestPyPI runs-on: ubuntu-latest + environment: + name: pypi-test + url: https://test.pypi.org/project/SoftLayer/ + permissions: + id-token: write steps: - uses: actions/checkout@master - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.11 - name: Install pypa/build run: >- python -m @@ -31,7 +37,6 @@ jobs: --outdir dist/ . - name: Publish đŸ“Ļ to Test PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.CGALLO_TEST_PYPI }} - repository_url: https://test.pypi.org/legacy/ \ No newline at end of file + repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a9827899a..3dca03b03 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.10',3.11,3.12,3.13,3.14] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -27,11 +27,11 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.14 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -41,14 +41,30 @@ jobs: analysis: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.14 - 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/.gitignore b/.gitignore index 5dd1975be..72fd99883 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ dist/* .cache .idea .pytest_cache/* +.vscode/* + +techbabble.xyz.crt +techbabble.xyz.csr +techbabble.xyz.icc +techbabble.xyz.key diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..dcede9b96 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +# This is an example configuration to enable detect-secrets in the pre-commit hook. +# Add this file to the root folder of your repository. +# +# Read pre-commit hook framework https://pre-commit.com/ for more details about the structure of config yaml file and how git pre-commit would invoke each hook. +# +# This line indicates we will use the hook from ibm/detect-secrets to run scan during committing phase. +repos: + - repo: https://github.com/ibm/detect-secrets + # If you desire to use a specific version of detect-secrets, you can replace `master` with other git revisions such as branch, tag or commit sha. + # 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.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. + # You may also run `pre-commit run detect-secrets` to preview the scan result. + # when "--baseline" without "--use-all-plugins", pre-commit scan with just plugins in baseline file + # when "--baseline" with "--use-all-plugins", pre-commit scan with all available plugins + # add "--fail-on-unaudited" to fail pre-commit for unaudited potential secrets + args: [--baseline, .secrets.baseline, --use-all-plugins] \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml index 73e4a4e5d..18d147019 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,6 +5,11 @@ # Required version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.10" + # Build documentation in the docs/ directory with Sphinx sphinx: builder: htmldir @@ -15,10 +20,11 @@ sphinx: # configuration: mkdocs.yml # Optionally build your docs in additional formats such as PDF and ePub -formats: all +# formats: all # Optionally set the version of Python and requirements required to build your docs python: - version: 3.7 install: - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 000000000..99a7c5ac2 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,770 @@ +{ + "exclude": { + "files": "^.secrets.baseline$", + "lines": null + }, + "generated_at": "2026-03-24T22:14:30Z", + "plugins_used": [ + { + "name": "AWSKeyDetector" + }, + { + "name": "ArtifactoryDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "base64_limit": 4.5, + "name": "Base64HighEntropyString" + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "BoxDetector" + }, + { + "name": "CloudantDetector" + }, + { + "ghe_instance": "github.ibm.com", + "name": "GheDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "hex_limit": 3, + "name": "HexHighEntropyString" + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "keyword_exclude": null, + "name": "KeywordDetector" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "results": { + "RELEASE.md": [ + { + "hashed_secret": "564e340cd48437d2dfe876ee154cc99dc4d0d137", + "is_secret": false, + "is_verified": false, + "line_number": 67, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/CLI/dns/record_add.py": [ + { + "hashed_secret": "826feff6caff89ca2f2408dce0f2d9caecf9dc5f", + "is_secret": false, + "is_verified": false, + "line_number": 63, + "type": "Base64 High Entropy String", + "verified_result": null + } + ], + "SoftLayer/CLI/user/list.py": [ + { + "hashed_secret": "71206af1d24cf9ddf0c9a804ef700ed7fb3cb5ce", + "is_secret": false, + "is_verified": false, + "line_number": 11, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Account.py": [ + { + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", + "is_secret": false, + "is_verified": false, + "line_number": 122, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "df51e37c269aa94d38f93e537bf6e2020b21406c", + "is_secret": false, + "is_verified": false, + "line_number": 1036, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Event_Log.py": [ + { + "hashed_secret": "2c0ceacd445f15ebc02315e18fb3ed8ec73a61a0", + "is_secret": false, + "is_verified": false, + "line_number": 25, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "f08bf4f915242a2700e861e4e073ab45dc745e92", + "is_secret": false, + "is_verified": false, + "line_number": 32, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc", + "is_secret": false, + "is_verified": false, + "line_number": 47, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "6e61399506056ac598fc283b3be0aecf80a51952", + "is_secret": false, + "is_verified": false, + "line_number": 61, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "806f21b4bc195ffd5749f295b83909d66a56ff38", + "is_secret": false, + "is_verified": false, + "line_number": 79, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "1c89f7ca3440fe5db16e3b0ffe414d11845331d9", + "is_secret": false, + "is_verified": false, + "line_number": 85, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "5eb37c21d01d15fab7b546ee8fd1b50080fef2a3", + "is_secret": false, + "is_verified": false, + "line_number": 96, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "99e9638f573f92843c387930bec48bc75c854b90", + "is_secret": false, + "is_verified": false, + "line_number": 103, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "ee85b0f2b6ab5557b3b240d3a454e449ab651ee2", + "is_secret": false, + "is_verified": false, + "line_number": 114, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Hardware.py": [ + { + "hashed_secret": "49901d945ad6da0f0af47691f305daf994d9d2c9", + "is_secret": false, + "is_verified": false, + "line_number": 43, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "e1f0942738bb56a9905ac28a05c381ba1ca0a4e2", + "is_secret": false, + "is_verified": false, + "line_number": 47, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "7288edd0fc3ffcbe93a0cf06e3568e28521687bc", + "is_secret": false, + "is_verified": false, + "line_number": 122, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Hardware_Server.py": [ + { + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", + "is_secret": false, + "is_verified": false, + "line_number": 54, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 274, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller.py": [ + { + "hashed_secret": "df51e37c269aa94d38f93e537bf6e2020b21406c", + "is_secret": false, + "is_verified": false, + "line_number": 34, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Message_Delivery_Email_Sendgrid.py": [ + { + "hashed_secret": "707296a56c05e7213079ef340c13c2f383471b92", + "is_secret": false, + "is_verified": false, + "line_number": 33, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Storage_Hub_Cleversafe_Account.py": [ + { + "hashed_secret": "87e3789cb5540dfb78446e7beec33649dc8940c5", + "is_secret": false, + "is_verified": false, + "line_number": 31, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "f80a4da46034866bb3b6eee29b45d084c5f7829b", + "is_secret": false, + "is_verified": false, + "line_number": 31, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "c8d74351f47fcc09d44ccf063ca535f1056ff5cf", + "is_secret": false, + "is_verified": false, + "line_number": 74, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "f055a8f21c40658e6a2bf2c2c112fce3fc059148", + "is_secret": false, + "is_verified": false, + "line_number": 74, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "6ef8455158f5f522d25a813c6a7082fab8f7d7cd", + "is_secret": false, + "is_verified": false, + "line_number": 84, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "c26cca6e5b560bb54203b63596e243baa3e0afcc", + "is_secret": false, + "is_verified": false, + "line_number": 84, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "1685a5baa79a864b222e6a013d285cc553dd2de8", + "is_secret": false, + "is_verified": false, + "line_number": 92, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Storage_Iscsi.py": [ + { + "hashed_secret": "ed4ad870c35e2c96f8b59bc6c12b0f1262175e38", + "is_secret": false, + "is_verified": false, + "line_number": 17, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Vlan_Firewall.py": [ + { + "hashed_secret": "9bc34549d565d9505b287de0cd20ac77be1d3f2c", + "is_secret": false, + "is_verified": false, + "line_number": 57, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Security_Certificate.py": [ + { + "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", + "is_secret": false, + "is_verified": false, + "line_number": 14, + "type": "Private Key", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Virtual_Guest.py": [ + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 936, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/full.conf": [ + { + "hashed_secret": "d332bc701dd6999e9de0ea46f3127031250634d3", + "is_secret": false, + "is_verified": false, + "line_number": 3, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/managers/vs_capacity.py": [ + { + "hashed_secret": "8af1f8146d96a3cd862281442d0d6c5cb6f8f9e5", + "is_secret": false, + "is_verified": false, + "line_number": 133, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "SoftLayer/transports/soap.py.unstable": [ + { + "hashed_secret": "813c25388cd13e54d03723a57f678007399997e2", + "is_secret": false, + "is_verified": false, + "line_number": 59, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "eb985bdb5ffec689d0019ea0a9443bea2105738a", + "is_secret": false, + "is_verified": false, + "line_number": 89, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/api/client.rst": [ + { + "hashed_secret": "89a6cfe2a229151e8055abee107d45ed087bbb4f", + "is_secret": false, + "is_verified": false, + "line_number": 50, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/block.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 18, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/hardware.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 36, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/nas.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 10, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/users.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 75, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/vs.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 262, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/config_file.rst": [ + { + "hashed_secret": "0f2f17651724aa4ec1676466b1e530992495a124", + "is_secret": false, + "is_verified": false, + "line_number": 25, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "798eabe0e81cecca592e52d37f2425494207a80f", + "is_secret": false, + "is_verified": false, + "line_number": 35, + "type": "Secret Keyword", + "verified_result": null + } + ], + "output.txt": [ + { + "hashed_secret": "81448fe273247b533b9f018e96c158cab7901247", + "is_secret": false, + "is_verified": false, + "line_number": 726, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "59b189070af751d4e93a749ccffb4ccfd2de7ab5", + "is_secret": false, + "is_verified": false, + "line_number": 1337, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "bbccdf2efb33b52e6c9d0a14dd70b2d415fbea6e", + "is_secret": false, + "is_verified": false, + "line_number": 1776, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "fe77c82bb2a42efeec9303600c8e7f6df56b6faf", + "is_secret": false, + "is_verified": false, + "line_number": 1923, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "4dc73741b9473168444fab7e680b439ba69f41ec", + "is_secret": false, + "is_verified": false, + "line_number": 3101, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/CLI/modules/firewall_tests.py": [ + { + "hashed_secret": "9bc34549d565d9505b287de0cd20ac77be1d3f2c", + "is_secret": false, + "is_verified": false, + "line_number": 90, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/CLI/modules/hardware/hardware_basic_tests.py": [ + { + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", + "is_secret": false, + "is_verified": false, + "line_number": 57, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/CLI/modules/securitygroup_tests.py": [ + { + "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc", + "is_secret": false, + "is_verified": false, + "line_number": 339, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "tests/api_tests.py": [ + { + "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", + "is_secret": false, + "is_verified": false, + "line_number": 82, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/auth_tests.py": [ + { + "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", + "is_secret": false, + "is_verified": false, + "line_number": 33, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/functional_tests.py": [ + { + "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", + "is_secret": false, + "is_verified": false, + "line_number": 31, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/block_tests.py": [ + { + "hashed_secret": "f7a9e24777ec23212c54d7a350bc5bea5477fdbb", + "is_secret": false, + "is_verified": false, + "line_number": 1077, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/hardware_tests.py": [ + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 673, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/image_tests.py": [ + { + "hashed_secret": "8de91b1f4c8ca32302ae101da16fb88fb127582a", + "is_secret": false, + "is_verified": false, + "line_number": 168, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "2da422d13be8072a8dcae1e46b36add9cb2372fa", + "is_secret": false, + "is_verified": false, + "line_number": 193, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/ipsec_tests.py": [ + { + "hashed_secret": "b310da45b1ebf444106a41b7832ab2fbe25dab41", + "is_secret": false, + "is_verified": false, + "line_number": 275, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "5b399c20d855de2450baab07ed09726b326cfeb1", + "is_secret": false, + "is_verified": false, + "line_number": 279, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/network_tests.py": [ + { + "hashed_secret": "2c0ceacd445f15ebc02315e18fb3ed8ec73a61a0", + "is_secret": false, + "is_verified": false, + "line_number": 545, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "f08bf4f915242a2700e861e4e073ab45dc745e92", + "is_secret": false, + "is_verified": false, + "line_number": 552, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "806f21b4bc195ffd5749f295b83909d66a56ff38", + "is_secret": false, + "is_verified": false, + "line_number": 584, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "1c89f7ca3440fe5db16e3b0ffe414d11845331d9", + "is_secret": false, + "is_verified": false, + "line_number": 590, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc", + "is_secret": false, + "is_verified": false, + "line_number": 597, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "tests/managers/object_storage_tests.py": [ + { + "hashed_secret": "2551e332f3a8c04696365d595601ddf806f4b799", + "is_secret": false, + "is_verified": false, + "line_number": 81, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "490a5c1209ddffbb772dfd6d9e8873f295362bcf", + "is_secret": false, + "is_verified": false, + "line_number": 81, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/vs/vs_capacity_tests.py": [ + { + "hashed_secret": "8af1f8146d96a3cd862281442d0d6c5cb6f8f9e5", + "is_secret": false, + "is_verified": false, + "line_number": 187, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "tests/managers/vs/vs_tests.py": [ + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 1149, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/transports/rest_tests.py": [ + { + "hashed_secret": "9878e362285eb314cfdbaa8ee8c300c285856810", + "is_secret": false, + "is_verified": false, + "line_number": 313, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/transports/soap_tests.py.unstable": [ + { + "hashed_secret": "8bb6118f8fd6935ad0876a3be34a717d32708ffd", + "is_secret": false, + "is_verified": false, + "line_number": 42, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "f0e99a0fcd86f5764d44e0518947046a29ca7245", + "is_secret": false, + "is_verified": false, + "line_number": 117, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/transports/xmlrpc_tests.py": [ + { + "hashed_secret": "f08c5dc4980df3c1237e88b872a2429dac6be328", + "is_secret": false, + "is_verified": false, + "line_number": 297, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "7e6a3680012346b94b54731e13d8a9ffa3790645", + "is_secret": false, + "is_verified": false, + "line_number": 383, + "type": "Secret Keyword", + "verified_result": null + } + ] + }, + "version": "0.13.1+ibm.62.dss", + "word_list": { + "file": null, + "hash": null + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecbd7ad68..45bce3aff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,27 @@ Docs are generated with [Sphinx](https://docs.readthedocs.io/en/latest/intro/get `make html` in the softlayer-python/docs directory, which should generate the HTML in `softlayer-python/docs/_build/html` for testing. +For windows, use: +``` +cd docs +python -m sphinx -T -E -b dirhtml -d _build/doctrees -D language=en . _build/html +``` + + +### Note + +If you get this error, or similar... you might just need to remove the `_build/html/*` directory and try again, that seems to generally work. +``` +docs\cli\hardware.rst:15: ERROR: Failed to import "cli" from "SoftLayer.CLI.hardware.cancel". The following exception was raised: +Traceback (most recent call last): + File "py311\Lib\site-packages\sphinx_click\ext.py", line 403, in _load_module + mod = __import__(module_name, globals(), locals(), [attr_name]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "SoftLayer\CLI\hardware\cancel.py", line 13, in + @click.command(cls=SoftLayer.CLI.command.SLCommand, ) + ^^^^^^^^^^^^^^^^^^^^^ +AttributeError: module 'SoftLayer.CLI' has no attribute 'command' +``` ## Unit Tests @@ -154,3 +175,27 @@ When doing testing of a code change, indicate this with a comment on the pull re :heavy_check: `slcli vs list --new-feature` :x: `slcli vs list --broken-feature` + + +### Secret Checking +This repo uses [IBM Detect-Secrets](https://github.com/IBM/detect-secrets) to prevent secrets from being committed to the codebase. If your commit is rejected because of a secret make sure to remove the secret and try again. If you need to mark the secret as a false positive to the following: + +``` +detect-secrets scan --update .secrets.baseline +git add .secrets.baseline +``` + +The first time you commit code, you may need to install detect-secrets, but hopefully that should be taken care of you by the git precommit hook. + +``` +$> git commit --message="#1997 adding secret baseline" +[INFO] Initializing environment for https://github.com/ibm/detect-secrets. +[INFO] Installing environment for https://github.com/ibm/detect-secrets. +[INFO] Once installed this environment will be reused. +[INFO] This may take a few minutes... +Detect secrets...........................................................Passed +[issues1997 11d3dcb5] #1997 adding secret baseline + 2 files changed, 791 insertions(+) + create mode 100644 .pre-commit-config.yaml + create mode 100644 .secrets.baseline +``` \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 50a35f039..000000000 --- a/Makefile +++ /dev/null @@ -1,192 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/softlayer-python.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/softlayer-python.qhc" - -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/softlayer-python" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/softlayer-python" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 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..29536d085 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 `_. @@ -179,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/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 29c5ac689..17fec6d94 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -7,18 +7,19 @@ """ # pylint: disable=invalid-name import time -import warnings +import concurrent.futures as cf import json import logging +import math import requests - from SoftLayer import auth as slauth from SoftLayer import config 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 @@ -27,11 +28,13 @@ __all__ = [ 'create_client_from_env', + 'employee_client', 'Client', 'BaseClient', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', 'IAMClient', + 'CertificateClient' ] VALID_CALL_ARGS = set(( @@ -43,10 +46,42 @@ 'raw_headers', 'limit', 'offset', - 'verify', + 'verify' )) +def _build_transport(url, proxy, timeout, user_agent, verify): + """Construct the appropriate transport based on the endpoint URL. + + Selects RestTransport when the URL contains '/rest', otherwise falls back + to XmlRpcTransport. Extracted to avoid duplicating this logic across + ``create_client_from_env``, ``employee_client``, and ``BaseClient``. + + :param str url: The API endpoint URL. + :param str proxy: Optional proxy URL. + :param timeout: Request timeout in seconds (``None`` means no timeout). + :param str user_agent: Optional User-Agent string override. + :param verify: SSL verification — ``True``, ``False``, or a path to a CA bundle. + :returns: A :class:`~SoftLayer.transports.RestTransport` or + :class:`~SoftLayer.transports.XmlRpcTransport` instance. + """ + if url is not None and '/rest' in url: + return transports.RestTransport( + endpoint_url=url, + proxy=proxy, + timeout=timeout, + user_agent=user_agent, + verify=verify, + ) + return transports.XmlRpcTransport( + endpoint_url=url, + proxy=proxy, + timeout=timeout, + user_agent=user_agent, + verify=verify, + ) + + def create_client_from_env(username=None, api_key=None, endpoint_url=None, @@ -59,7 +94,7 @@ def create_client_from_env(username=None, verify=True): """Creates a SoftLayer API client using your environment. - Settings are loaded via keyword arguments, environemtal variables and + Settings are loaded via keyword arguments, environmental variables and config file. :param username: an optional API username if you wish to bypass the @@ -101,25 +136,13 @@ def create_client_from_env(username=None, config_file=config_file) if transport is None: - url = settings.get('endpoint_url') - 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=settings.get('endpoint_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=settings.get('endpoint_url'), - proxy=settings.get('proxy'), - timeout=settings.get('timeout'), - user_agent=user_agent, - verify=verify, - ) + transport = _build_transport( + url=settings.get('endpoint_url'), + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) # If we have enough information to make an auth driver, let's do it if auth is None and settings.get('username') and settings.get('api_key'): @@ -142,13 +165,86 @@ 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. - Deprecated in favor of create_client_from_env() + Settings are loaded via keyword arguments, environmental variables and config file. + + :param username: your user ID + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication + :param endpoint_url: the API endpoint base URL you wish to connect to. + Must contain 'internal'. 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. + DO NOT SET TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS. """ - warnings.warn("use SoftLayer.create_client_from_env() instead", - DeprecationWarning) + # Pass caller-supplied verify so it is not silently discarded; the config + # file value will take precedence if present (via get_client_settings). + settings = config.get_client_settings(username=username, + api_key=None, + endpoint_url=endpoint_url, + timeout=timeout, + proxy=proxy, + verify=verify, + config_file=config_file) + + url = settings.get('endpoint_url', '') + # Honour the config-file value; fall back to the caller-supplied default. + verify = settings.get('verify', verify) + + if 'internal' not in url: + raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.") + + # url is guaranteed non-empty here (the guard above ensures it contains + # 'internal'), so no additional None-check is needed. + if transport is None: + transport = _build_transport( + url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + + # Resolve all settings-derived credentials together before auth selection. + if access_token is None: + access_token = settings.get('access_token') + user_id = settings.get('userid') + + # Select the appropriate auth driver only when the caller has not already + # supplied one. A single return keeps construction separate from selection. + if auth is None: + if settings.get('auth_cert'): + auth = slauth.X509Authentication(settings.get('auth_cert'), verify) + elif access_token and user_id: + auth = slauth.EmployeeAuthentication(user_id, access_token) + else: + # No credentials available — caller must authenticate explicitly + # (e.g. via EmployeeClient.authenticate_with_internal). + LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.") + + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + + +def Client(**kwargs): + """Get a SoftLayer API Client using environmental settings.""" return create_client_from_env(**kwargs) @@ -156,19 +252,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: @@ -179,7 +286,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 @@ -188,14 +295,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 @@ -258,8 +363,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): @@ -285,17 +389,10 @@ 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: - extra_headers = self.auth.get_headers() - if extra_headers: - warnings.warn("auth.get_headers() is deprecated and will be " - "removed in the next major version", - DeprecationWarning) - request.headers.update(extra_headers) - request = self.auth.get_request(request) request.headers.update(kwargs.get('headers', {})) @@ -324,6 +421,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 @@ -352,6 +450,43 @@ def iter_call(self, service, method, *args, **kwargs): offset += limit + def cf_call(self, service, method, *args, **kwargs): + """Uses threads to iterate through API calls. + + :param service: the name of the SoftLayer API service + :param method: the method to call on the service + :param integer limit: result size for each API call (defaults to 100) + :param \\*args: same optional arguments that ``Service.call`` takes + :param \\*\\*kwargs: same optional keyword arguments that ``Service.call`` takes + """ + limit = kwargs.pop('limit', 100) + offset = kwargs.pop('offset', 0) + + if limit <= 0: + raise AttributeError("Limit size should be greater than zero.") + # This initial API call is to determine how many API calls we need to make after this first one. + first_call = self.call(service, method, offset=offset, limit=limit, *args, **kwargs) + + # This was not a list result, just return it. + if not isinstance(first_call, transports.SoftLayerListResult): + return first_call + # How many more API calls we have to make + # +1 at the end here because 'range' doesn't include the stop number + api_calls = math.ceil((first_call.total_count - limit) / limit) + 1 + + def this_api(offset): + """Used to easily call executor.map() on this fuction""" + return self.call(service, method, offset=offset, limit=limit, *args, **kwargs) + + with cf.ThreadPoolExecutor(max_workers=10) as executor: + future_results = {} + offset_map = [x * limit for x in range(1, api_calls)] + future_results = list(executor.map(this_api, offset_map)) + # Append the results in the order they were called + for call_result in future_results: + first_call = first_call + call_result + return first_call + def __repr__(self): return "Client(transport=%r, auth=%r)" % (self.transport, self.auth) @@ -361,6 +496,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 @@ -545,6 +705,93 @@ 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 self.account_id and not kwargs.get('id', False): + 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) + print("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/bandwidth/pools_create.py b/SoftLayer/CLI/bandwidth/pools_create.py index 477ce2225..e14aa2bba 100644 --- a/SoftLayer/CLI/bandwidth/pools_create.py +++ b/SoftLayer/CLI/bandwidth/pools_create.py @@ -25,10 +25,11 @@ def check_region_param(ctx, param, value): # pylint: disable=unused-argument - """Check if provided region is region group or part of region""" + """Check if provided region is region group or part of region - # :params string value: Region or Region-Groups - # return string Region-Groups + :params string value: Region or Region-Groups + return string Region-Groups + """ region_group = None for key in location_groups: @@ -53,10 +54,11 @@ def cli(env, name, region): """Create bandwidth pool. Region can be the full zone name 'SJC/DAL/WDC/TOR/MON', or just a single datacenter like 'SJC'. + Example:: slcli bandwidth pool-create --name testPool --region DAL slcli bandwidth pool-create --name testPool --region SJC/DAL/WDC/TOR/MON - """ + """ manager = BandwidthManager(env.client) locations = manager.get_location_group() 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 b38f89d7c..34c50e549 100644 --- a/SoftLayer/CLI/command.py +++ b/SoftLayer/CLI/command.py @@ -28,6 +28,10 @@ class OptionHighlighter(RegexHighlighter): r"(?P