diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..78f5fd340 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# 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 +# https://gist.github.com/tekin/12500956bd56784728e490d8cef9cb81 +*.css diff=css +*.html diff=html +*.py diff=python +*.md diff=markdown + + +# Declare files that will always have CRLF line endings on checkout. +*.sln text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..e74b1b6ed --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Use `allow` to specify which dependencies to maintain +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-update + +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "pip prod" + prefix-development: "pip dev" + include: "scope" \ No newline at end of file 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 c713212ee..f09354735 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,27 +1,27 @@ -name: documentation - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.8] - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r tools/test-requirements.txt - - name: Documentation Checks - run: | - python docCheck.py +name: documentation + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.11] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tools/test-requirements.txt + - name: Documentation Checks + run: | + python docCheck.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b009483c3..95afeced1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,34 +1,27 @@ -name: Release Snapcraft and PyPi (Testing) +# 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: release: types: [published] jobs: - snap-release: - runs-on: ubuntu-18.04 - strategy: - matrix: - arch: ['armhf','amd64','arm64','ppc64el','s390x','i386'] - steps: - - name: Install Snapcraft - uses: samuelmeuli/action-snapcraft@v1.2.0 - with: - snapcraft_token: ${{ secrets.snapcraft_token }} - - name: Push to stable - run: | - VERSION=`snapcraft list-revisions slcli --arch ${{ matrix.arch }} | grep "edge\*" | awk '{print $1}'` - echo Publishing $VERSION on ${{ matrix.arch }} - snapcraft release slcli $VERSION stable 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.11 + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.11 - name: Install pypa/build run: >- python -m @@ -43,9 +36,6 @@ jobs: --wheel --outdir dist/ . - - name: Publish 📦 to Test PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.CGALLO_TEST_PYPI }} - repository_url: https://test.pypi.org/legacy/ + - name: 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 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 aea906c54..70245307b 100644 --- a/.github/workflows/test_pypi_release.yml +++ b/.github/workflows/test_pypi_release.yml @@ -1,37 +1,42 @@ -# https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ - -name: Publish 📦 to TestPyPI - -on: - push: - branches: [test-pypi ] - -jobs: - build-n-publish: - name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install pypa/build - run: >- - python -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - . - - name: Publish 📦 to Test PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.CGALLO_TEST_PYPI }} - repository_url: https://test.pypi.org/legacy/ \ No newline at end of file +# 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: TEST Publish 📦 to TestPyPI + +on: + push: + branches: [test-pypi] + +jobs: + build-n-publish: + 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.11 + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + - name: Publish 📦 to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7d6d35a67..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.6,3.7,3.8,3.9] + python-version: [3.8,3.9,'3.10',3.11,3.12] 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.12 - 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.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/.gitignore b/.gitignore index e34d2250a..5dd1975be 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,3 @@ dist/* .cache .idea .pytest_cache/* -slcli diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..0cbadf756 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Christopher Gallo 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..f0aee0650 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,770 @@ +{ + "exclude": { + "files": "^.secrets.baseline$", + "lines": null + }, + "generated_at": "2025-06-11T21:28:32Z", + "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": 81, + "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/CHANGELOG.md b/CHANGELOG.md index cf0eb2848..abee1bea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,221 @@ # Change Log +## [6.1.6] - 2023-03-27 + +From now on changes will be published only on GitHub https://github.com/softlayer/softlayer-python/releases + + +## [6.1.3] - 2022-11-30 + +#### What's Changed +* New Command: Hardware notifications by @caberos in https://github.com/softlayer/softlayer-python/pull/1756 +* New Command: virtual notifications by @caberos in https://github.com/softlayer/softlayer-python/pull/1758 +* Change regex in rich text in simple option in help text by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1759 +* pip prod(deps): bump rich from 12.5.1 to 12.6.0 by @dependabot in https://github.com/softlayer/softlayer-python/pull/1760 +* add more information to vs credentials by @caberos in https://github.com/softlayer/softlayer-python/pull/1762 +* Fixed maxint issue by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1765 +* Added csv output format by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1766 +* add more information on hw credentials by @caberos in https://github.com/softlayer/softlayer-python/pull/1767 +* Delete twitter link in documentation by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1769 +* new command hw create-credential by @caberos in https://github.com/softlayer/softlayer-python/pull/1774 +* fix the hw credential error by @caberos in https://github.com/softlayer/softlayer-python/pull/1781 +* Added test suite for py311 by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1784 +* New feature to change theme in slcli like dark, light o maintain in default by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1775 +* Match `virtual detail --prices` option with `hardware detail --prices` option by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1780 +* Fixing preset-list pricing table by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1782 +* fix the call api cannot handle empty results by @caberos in https://github.com/softlayer/softlayer-python/pull/1789 +* Debug output changed to a valid JSON by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1791 +* slcli hw add-notifications crashes with a bad user by @caberos in https://github.com/softlayer/softlayer-python/pull/1786 +* del-notification commands, rename the commands to notifications-add by @caberos in https://github.com/softlayer/softlayer-python/pull/1785 +* Add --extras to slcli order quote by @caberos in https://github.com/softlayer/softlayer-python/pull/1792 +* An error is displaying for volume with replica in slcli for Active pr… by @caberos in https://github.com/softlayer/softlayer-python/pull/1794 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.1.2...v6.1.3 + + +## [6.1.2] - 2022-09-23 + +#### What's Changed +* Snapcraft: Updated to Core22 and add homeishome-launch by @kz6fittycent in https://github.com/softlayer/softlayer-python/pull/1740 +* Add status, create date and domain columns in `slcli vs list command` by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1728 +* New command: ipsec cancel by @caberos in https://github.com/softlayer/softlayer-python/pull/1729 +* New command: subnet clear-route by @caberos in https://github.com/softlayer/softlayer-python/pull/1738 +* Deprecate slcli hw guests by @caberos in https://github.com/softlayer/softlayer-python/pull/1736 +* Remove real usersnames from test fixtrues by @caberos in https://github.com/softlayer/softlayer-python/pull/1743 +* Fix tox request.get hangout issue by @caberos in https://github.com/softlayer/softlayer-python/pull/1746 +* add vs user-access command by @caberos in https://github.com/softlayer/softlayer-python/pull/1741 +* Update Help message for commands that take in multiple arguments by @caberos in https://github.com/softlayer/softlayer-python/pull/1748 +* Error with slcli order item-list by @caberos in https://github.com/softlayer/softlayer-python/pull/1751 +* deprecate sl `autoscale` by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1753 +* Unhandled error running a subcommand in slcli by @caberos in https://github.com/softlayer/softlayer-python/pull/1754 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.1.1...v6.1.2 + + +## [6.1.1] - 2022-08-18 + +#### What's Changed +* v6.1.0 Changelog and version bump by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1674 +* item-list fix by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1679 +* updating release job to actually publish to pypi by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1680 +* Update command - slcli object-storage endpoints by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1685 +* add the block volume-options command by @caberos in https://github.com/softlayer/softlayer-python/pull/1681 +* add the file volume-options command by @caberos in https://github.com/softlayer/softlayer-python/pull/1684 +* fixed issues where a message warned users about closing datacenter by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1688 +* Enable --format=raw and fixes table width by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1689 +* Update `slcli hardware sensor` by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1691 +* Improved successful response to command - slcli vs cancel by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1695 +* Fixed an issue with printing tables that contained empty items by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1697 +* Added a dependabot scanner by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1699 +* block|file volume-options improvements by @caberos in https://github.com/softlayer/softlayer-python/pull/1700 +* Option create-options in commands hardware and dedicatedhost fixed by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1703 +* pip prod(deps): bump rich from 12.3.0 to 12.5.1 by @dependabot in https://github.com/softlayer/softlayer-python/pull/1704 +* block/file volume-options improvements 2 by @caberos in https://github.com/softlayer/softlayer-python/pull/1702 +* New command ipsec order by @caberos in https://github.com/softlayer/softlayer-python/pull/1698 +* block/file volume-options improvement 3 by @caberos in https://github.com/softlayer/softlayer-python/pull/1705 +* Command slcli vlan create - displaying an error message by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1707 +* New Command: user device-access by @caberos in https://github.com/softlayer/softlayer-python/pull/1712 +* Command slcli vlan edit accept that we do not send any parameters by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1709 +* Updated command - slcli vlan list by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1713 +* `slcli block subnets-list` command display an error message by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1716 +* add user remove-access command by @caberos in https://github.com/softlayer/softlayer-python/pull/1717 +* Add Devices with Trunks to vlan detail by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1721 +* slcli hardware reflash-firmware command does not display success message by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1724 +* Fix bug with command - slcli cdn edit by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1726 + +#### New Contributors +* @dependabot made their first contribution in https://github.com/softlayer/softlayer-python/pull/1704 + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.1.0...v6.1.1 + + + +## [6.1.0] - 2022-06-30 + +#### Major Updates +* [Rich](https://github.com/Textualize/rich) tables by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1646 +* [Rich](https://github.com/Textualize/rich) Text support by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1635 + +Rich Text and Rich Tables will modernize the output of the SLCLI to be a little nicer to look at, with colors and other highlighting. + +![image](https://user-images.githubusercontent.com/7408017/176753783-f6a4a43a-53ac-4600-a24f-21362f152747.png) +![image](https://user-images.githubusercontent.com/7408017/176753845-32af33f0-454f-4bab-ac63-1ae3db788ede.png) + + +#### What's Changed +* slcli licenses is missing the help text by @caberos in https://github.com/softlayer/softlayer-python/pull/1605 +* Add a warning if user orders in a POD that is being closed by @caberos in https://github.com/softlayer/softlayer-python/pull/1600 +* updated number of updates in the command account event-detail by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1609 +* Add an orderBy filter to slcli vlan list by @caberos in https://github.com/softlayer/softlayer-python/pull/1599 +* Add options to print a specific table in command slcli account events by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1611 +* Update global ip assign/unassign to use new API by @caberos in https://github.com/softlayer/softlayer-python/pull/1614 +* Ability to route/unroute subnets by @caberos in https://github.com/softlayer/softlayer-python/pull/1615 +* Improved successful response to command - slcli account cancel-item by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1617 +* Improved successful response to command - slcli virtual edit by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1618 +* Improved successful response to command - slcli vlan cancel by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1619 +* Mishandling of domain and hostname data in `slcli account item-detail` by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1621 +* Unable to get VSI details when last TXN is "Software install is finis… by @caberos in https://github.com/softlayer/softlayer-python/pull/1625 +* new command on autoscale delete by @caberos in https://github.com/softlayer/softlayer-python/pull/1628 +* Incorrect table title is displayed when an Auto Scale Group is scaled to reduce members by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1629 +* slcli autoscale create by @caberos in https://github.com/softlayer/softlayer-python/pull/1623 +* Soap transport by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1626 +* fix issue on loadbal order command by @caberos in https://github.com/softlayer/softlayer-python/pull/1633 +* Policy is not added when an AutoScale Group is created by @caberos in https://github.com/softlayer/softlayer-python/pull/1637 +* When `slcli event-log` not return any event log the command display an error by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1641 +* add new columns on vlan list(premium, tags) by @caberos in https://github.com/softlayer/softlayer-python/pull/1645 +* fixed documentation build issues by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1648 +* Improved successful response to command - slcli licenses cancel by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1653 +* update the firewall list by @caberos in https://github.com/softlayer/softlayer-python/pull/1649 +* Updated readme by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1656 +* Update `slcli firewall detail` to handle multi vlan firewalls by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1651 +* New command for getting duplicate convert status by @ko101 in https://github.com/softlayer/softlayer-python/pull/1655 +* Fixed TOX errors by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1661 +* add a new feature to get all cloud object storage by @caberos in https://github.com/softlayer/softlayer-python/pull/1662 +* Update `slcli report bandwidth` command by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1664 +* add firewall monitoring command by @caberos in https://github.com/softlayer/softlayer-python/pull/1657 +* add a new command on block object-storage details by @caberos in https://github.com/softlayer/softlayer-python/pull/1666 +* slcli account bandwidth-pools-detail command displays an error with b… by @caberos in https://github.com/softlayer/softlayer-python/pull/1670 +* new feature block object-storage permissions command by @caberos in https://github.com/softlayer/softlayer-python/pull/1668 +* fix the vlan table by @caberos in https://github.com/softlayer/softlayer-python/pull/1672 + +#### New Contributors +* @BrianSantivanez made their first contribution in https://github.com/softlayer/softlayer-python/pull/1629 + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.0.2...v6.1.0 + + +### [6.0.2] - 2022-03-30 + +#### What's Changed +* New Command slcli hardware|virtual monitoring by @caberos in https://github.com/softlayer/softlayer-python/pull/1593 +* When listing datacenters/pods, mark those that are closing soon. by @caberos in https://github.com/softlayer/softlayer-python/pull/1597 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.0.1...v6.0.2 + +## [6.0.1] - 2022-03-11 + + +#### What's Changed +* Replace the use of ptable with prettytable by @dvzrv in https://github.com/softlayer/softlayer-python/pull/1584 +* Bandwidth pool management by @caberos in https://github.com/softlayer/softlayer-python/pull/1582 +* Add id in the result in the command bandwidth-pools by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1586 +* Datacenter closure report by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1592 +* fix to errors in slcli hw create-options by @caberos in https://github.com/softlayer/softlayer-python/pull/1594 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v5.9.9...v6.0.1 + +6.0.0 was skipped. + +## [5.9.9] - 2022-02-04 + +https://github.com/softlayer/softlayer-python/compare/v5.9.8...v5.9.9 + +#### Improvements +- Add loadbalancer timeout values #1576 +- Add pricing date to slcli order preset-list #1578 + +#### New Commands +- `slcli vlan create-options` add new feature on vlan #1572 +- `slcli account bandwidth-pools` Bandwidth pool features #1579 + +## [5.9.8] - 2021-12-07 + +https://github.com/softlayer/softlayer-python/compare/v5.9.7...v5.9.8 + +#### Improvements + +- Fix code blocks formatting of The Solution section docs #1534 +- Add retry decorator to documentation #1535 +- Updated utility docs #1536 +- Add Exceptions to Documentation #1537 +- Forces specific encoding on XMLRPC requests #1543 +- Add sensor data to hardware #1544 +- Ignoring f-string related messages for tox for now #1548 +- Fix account events #1546 +- Improved loadbal details #1549 +- Fix initialized accountmanger #1552 +- Fix hw billing reports 0 items #1556 +- Update API docs link and remove travisCI mention #1557 +- Fix errors with vs bandwidth #1563 +- Add Item names to vs billing report #1564 +- Mapping is now in collections.abc #1565 +- fix vs placementgroup list #1567 +- fixed up snapshot-notification cli commands #1569 + +#### New Commands +- loadbal l7policies #1553 + + ` slcli loadbal l7policies --protocol-id` + + `slcli loadbal l7policies` +- Snapshot notify #1554 + + `slcli file|block snapshot-set-notification` + + `slcli file|block snapshot-get-notification-status` + + + ## [5.9.7] - 2021-08-04 https://github.com/softlayer/softlayer-python/compare/v5.9.6...v5.9.7 @@ -173,7 +389,7 @@ https://github.com/softlayer/softlayer-python/compare/v5.8.9...v5.9.0 - #1318 add Drive number in guest drives details using the device number - #1323 add vs list hardware and all option -## [5.8.9] - 2020-07-06 +## [5.8.9] - 2020-07-06 https://github.com/softlayer/softlayer-python/compare/v5.8.8...v5.8.9 - #1252 Automated Snap publisher diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ec9136a1..45bce3aff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,8 +40,29 @@ Manager methods should have a decent docblock describing any parameters and what Docs are generated with [Sphinx](https://docs.readthedocs.io/en/latest/intro/getting-started-with-sphinx.html) and once Sphinx is setup, you can simply do -`make html` in the softlayer-python/docs directory, which should generate the HTML in softlayer-python/docs/_build/html for testing. +`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 15d5bcca3..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 `_. @@ -98,6 +92,7 @@ is equivalent to $ slcli -vvv --format=json vs list + Getting Help ------------ Bugs and feature requests about this library should have a `GitHub issue `_ opened about them. @@ -110,6 +105,12 @@ Examples A curated list of examples on how to use this library can be found at `SLDN `_ +Development +----------- +To get started working with this project please read the `CONTRIBUTING `_ document. + +You can quickly test local changes by running the './slcli' file, which will load the local softlayer-python code instead of the system's softlayer-python codebase. + Debugging --------- To get the exact API call that this library makes, you can do the following. @@ -153,11 +154,16 @@ If you are using the library directly in python, you can do something like this. System Requirements ------------------- -* Python 3.5, 3.6, 3.7, 3.8, or 3.9. +* Python 3.8, 3.9, or 3.10. * A valid SoftLayer API username and key. * A connection to SoftLayer's private network is required to use our private network API endpoints. +Python 3.6 Support +------------------ +As of version 6.0.0 SoftLayer-Python will no longer support python3.6, which is `End of Life as of 2022 `_. +If you cannot install python 3.8+ for some reason, you will need to use a version of softlayer-python <= 6.0.0 + Python 2.7 Support ------------------ As of version 5.8.0 SoftLayer-Python will no longer support python2.7, which is `End Of Life as of 2020 `_ . @@ -167,12 +173,14 @@ If you cannot install python 3.6+ for some reason, you will need to use a versio Python Packages --------------- -* ptable >= 0.9.2 -* click >= 7 -* requests >= 2.20.0 +* click >= 8.0.4 +* requests >= 2.32.2 * prompt_toolkit >= 2 * pygments >= 2.0.0 * urllib3 >= 1.24 +* rich == 12.3.0 + +*NOTE* If `ptable` (not prettytable) is installed, this will cause issues rendering tables. Copyright --------- 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 21f21ffc6..cff277286 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,7 +46,7 @@ 'raw_headers', 'limit', 'offset', - 'verify', + 'verify' )) @@ -142,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. - Deprecated in favor of create_client_from_env() + Settings are loaded via keyword arguments, environemtal variables and config file. + + :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) @@ -156,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: @@ -177,25 +266,23 @@ def __init__(self, auth=None, transport=None, config_file=None): endpoint_url=url, proxy=self.settings['softlayer'].get('proxy'), # prevents an exception incase timeout is a float number. - timeout=int(self.settings['softlayer'].getfloat('timeout')), + 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 transport = transports.XmlRpcTransport( endpoint_url=url, proxy=self.settings['softlayer'].get('proxy'), - timeout=int(self.settings['softlayer'].getfloat('timeout')), + 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 +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): @@ -285,17 +371,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 +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 @@ -352,6 +432,42 @@ 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 + api_calls = math.ceil((first_call.total_count - limit) / limit) + + 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 +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 @@ -545,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/billing_items.py b/SoftLayer/CLI/account/billing_items.py index 32bc6c271..9f754b1d4 100644 --- a/SoftLayer/CLI/account/billing_items.py +++ b/SoftLayer/CLI/account/billing_items.py @@ -1,60 +1,66 @@ -"""Lists all active billing items on this account. See https://cloud.ibm.com/billing/billing-items""" -# :license: MIT, see LICENSE for more details. -import click - -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer.managers.account import AccountManager as AccountManager -from SoftLayer import utils - - -@click.command() -@environment.pass_env -def cli(env): - """Lists billing items with some other useful information. - - Similiar to https://cloud.ibm.com/billing/billing-items - """ - - manager = AccountManager(env.client) - items = manager.get_account_billing_items() - table = item_table(items) - - env.fout(table) - - -def item_table(items): - """Formats a table for billing items""" - table = formatting.Table([ - "Id", - "Create Date", - "Cost", - "Category Code", - "Ordered By", - "Description", - "Notes" - ], title="Billing Items") - table.align['Description'] = 'l' - table.align['Category Code'] = 'l' - for item in items: - description = item.get('description') - fqdn = "{}.{}".format(item.get('hostName', ''), item.get('domainName', '')) - if fqdn != ".": - description = fqdn - user = utils.lookup(item, 'orderItem', 'order', 'userRecord') - ordered_by = "IBM" - create_date = utils.clean_time(item.get('createDate'), in_format='%Y-%m-%d', out_format='%Y-%m-%d') - if user: - # ordered_by = "{} ({})".format(user.get('displayName'), utils.lookup(user, 'userStatus', 'name')) - ordered_by = user.get('displayName') - - table.add_row([ - item.get('id'), - create_date, - item.get('nextInvoiceTotalRecurringAmount'), - item.get('categoryCode'), - ordered_by, - utils.trim_to(description, 50), - utils.trim_to(item.get('notes', 'None'), 40), - ]) - return table +"""Lists all active billing items on this account. See https://cloud.ibm.com/billing/billing-items""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + + +@click.command(cls=SLCommand) +@click.option('--create', '-c', help='The date the billing item was created.') +@click.option('--ordered', '-o', help='Name that ordered the item') +@click.option('--category', '-C', help='Category name') +@environment.pass_env +def cli(env, create, category, ordered): + """Lists billing items with some other useful information. + + Similiar to https://cloud.ibm.com/billing/billing-items + """ + + manager = AccountManager(env.client) + items = manager.get_account_billing_items(create, category) + table = item_table(items, ordered) + + env.fout(table) + + +def item_table(items, ordered=None): + """Formats a table for billing items""" + table = formatting.Table([ + "Id", + "Create Date", + "Cost", + "Category Code", + "Ordered By", + "Description", + "Notes" + ], title="Billing Items") + table.align['Description'] = 'l' + table.align['Category Code'] = 'l' + for item in items: + description = item.get('description') + fqdn = f"{item.get('hostName', '')}.{item.get('domainName', '')}" + if fqdn != ".": + description = fqdn + user = utils.lookup(item, 'orderItem', 'order', 'userRecord') + ordered_by = "IBM" + create_date = utils.clean_time(item.get('createDate'), in_format='%Y-%m-%d', out_format='%Y-%m-%d') + if user: + # ordered_by = "{} ({})".format(user.get('displayName'), utils.lookup(user, 'userStatus', 'name')) + ordered_by = user.get('displayName') + if ordered: + if ordered != ordered_by: + continue + table.add_row([ + item.get('id'), + create_date, + item.get('nextInvoiceTotalRecurringAmount'), + item.get('categoryCode'), + ordered_by, + utils.trim_to(description, 50), + utils.trim_to(item.get('notes', 'None'), 40), + ]) + return table diff --git a/SoftLayer/CLI/account/cancel_item.py b/SoftLayer/CLI/account/cancel_item.py index de0fa446b..5b2b9b3df 100644 --- a/SoftLayer/CLI/account/cancel_item.py +++ b/SoftLayer/CLI/account/cancel_item.py @@ -1,18 +1,24 @@ -"""Cancels a billing item.""" -# :license: MIT, see LICENSE for more details. -import click - -from SoftLayer.CLI import environment -from SoftLayer.managers.account import AccountManager as AccountManager - - -@click.command() -@click.argument('identifier') -@environment.pass_env -def cli(env, identifier): - """Cancels a billing item.""" - - manager = AccountManager(env.client) - item = manager.cancel_item(identifier) - - env.fout(item) +"""Cancels a billing item.""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.managers.account import AccountManager as AccountManager + + +@click.command(cls=SLCommand) +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Cancel the resource or service for a billing item. + + By default the billing item will be canceled on the next bill date and + reclaim of the resource will begin shortly after the cancellation + """ + + manager = AccountManager(env.client) + item = manager.cancel_item(identifier) + + if item: + env.fout(f"Item: {identifier} was cancelled.") diff --git a/SoftLayer/CLI/account/event_detail.py b/SoftLayer/CLI/account/event_detail.py index 2c1ee80c2..38a29c1bb 100644 --- a/SoftLayer/CLI/account/event_detail.py +++ b/SoftLayer/CLI/account/event_detail.py @@ -3,13 +3,14 @@ import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers.account import AccountManager as AccountManager from SoftLayer import utils -@click.command() +@click.command(cls=SLCommand) @click.argument('identifier') @click.option('--ack', is_flag=True, default=False, help="Acknowledge Event. Doing so will turn off the popup in the control portal") @@ -65,9 +66,9 @@ def update_table(event): """Formats a basic event update table""" update_number = 0 for update in event.get('updates', []): + update_number = update_number + 1 header = "======= Update #%s on %s =======" % (update_number, utils.clean_time(update.get('startDate'))) click.secho(header, fg='green') - update_number = update_number + 1 text = update.get('contents') # deals with all the \r\n from the API click.secho(utils.clean_splitlines(text)) diff --git a/SoftLayer/CLI/account/events.py b/SoftLayer/CLI/account/events.py index 7d4803b42..3dc59e329 100644 --- a/SoftLayer/CLI/account/events.py +++ b/SoftLayer/CLI/account/events.py @@ -2,32 +2,52 @@ # :license: MIT, see LICENSE for more details. import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers.account import AccountManager as AccountManager from SoftLayer import utils -@click.command() +@click.command(cls=SLCommand) +@click.option('--announcement', is_flag=True, default=False, + help="Show only announcement events.") @click.option('--ack-all', is_flag=True, default=False, - help="Acknowledge every upcoming event. Doing so will turn off the popup in the control portal") + help="Acknowledge every upcoming event. Doing so will turn off the popup in the control portal.") +@click.option('--date-min', help="Earliest date to retrieve events for [MM/DD/YYYY]. Default: 2 days ago.") +@click.option('--planned', is_flag=True, default=False, + help="Show only planned events.") +@click.option('--unplanned', is_flag=True, default=False, + help="Show only unplanned events.") @environment.pass_env -def cli(env, ack_all): +def cli(env, ack_all, planned, unplanned, announcement, date_min): """Summary and acknowledgement of upcoming and ongoing maintenance events""" + if date_min: + utils.verify_date(date_min) + manager = AccountManager(env.client) - planned_events = manager.get_upcoming_events("PLANNED") - unplanned_events = manager.get_upcoming_events("UNPLANNED_INCIDENT") - announcement_events = manager.get_upcoming_events("ANNOUNCEMENT") + planned_events = manager.get_upcoming_events("PLANNED", date_min) + unplanned_events = manager.get_upcoming_events("UNPLANNED_INCIDENT", date_min) + announcement_events = manager.get_upcoming_events("ANNOUNCEMENT", date_min) add_ack_flag(planned_events, manager, ack_all) - env.fout(planned_event_table(planned_events)) - add_ack_flag(unplanned_events, manager, ack_all) - env.fout(unplanned_event_table(unplanned_events)) - add_ack_flag(announcement_events, manager, ack_all) - env.fout(announcement_event_table(announcement_events)) + + if planned: + env.fout(planned_event_table(planned_events)) + + if unplanned: + env.fout(unplanned_event_table(unplanned_events)) + + if announcement: + env.fout(announcement_event_table(announcement_events)) + + if not planned and not unplanned and not announcement: + env.fout(planned_event_table(planned_events)) + env.fout(unplanned_event_table(unplanned_events)) + env.fout(announcement_event_table(announcement_events)) def add_ack_flag(events, manager, ack_all): diff --git a/SoftLayer/CLI/account/hook_create.py b/SoftLayer/CLI/account/hook_create.py new file mode 100644 index 000000000..d046c38dd --- /dev/null +++ b/SoftLayer/CLI/account/hook_create.py @@ -0,0 +1,31 @@ +"""Order/create a provisioning script.""" +# :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('--name', '-N', required=True, prompt=True, help="The name of the hook.") +@click.option('--uri', '-U', required=True, prompt=True, help="The endpoint that the script will be downloaded") +@environment.pass_env +def cli(env, name, uri): + """Order/create a provisioning script.""" + + manager = SoftLayer.AccountManager(env.client) + + provisioning = manager.create_provisioning(name, uri) + + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + + table.add_row(['Id', provisioning.get('id')]) + table.add_row(['Name', provisioning.get('name')]) + table.add_row(['Created', provisioning.get('createDate')]) + table.add_row(['Uri', provisioning.get('uri')]) + + env.fout(table) diff --git a/SoftLayer/CLI/account/hook_delete.py b/SoftLayer/CLI/account/hook_delete.py new file mode 100644 index 000000000..c4eda42ba --- /dev/null +++ b/SoftLayer/CLI/account/hook_delete.py @@ -0,0 +1,24 @@ +"""Delete a provisioning script""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer + +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.managers.account import AccountManager as AccountManager + + +@click.command(cls=SLCommand) +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Delete a provisioning script""" + + manager = AccountManager(env.client) + + try: + manager.delete_provisioning(identifier) + click.secho("%s deleted successfully" % identifier, fg='green') + except SoftLayer.SoftLayerAPIError as ex: + click.secho("Failed to delete %s\n%s" % (identifier, ex), fg='red') diff --git a/SoftLayer/CLI/account/hooks.py b/SoftLayer/CLI/account/hooks.py new file mode 100644 index 000000000..582ae4c24 --- /dev/null +++ b/SoftLayer/CLI/account/hooks.py @@ -0,0 +1,25 @@ +"""Show all Provisioning Scripts.""" +# :license: MIT, see LICENSE for more details. +import click + + +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import account + + +@click.command(cls=SLCommand) +@environment.pass_env +def cli(env): + """Show all Provisioning Scripts.""" + + manager = account.AccountManager(env.client) + hooks = manager.get_provisioning_scripts() + + table = formatting.Table(["Id", "Name", "Uri"]) + + for hook in hooks: + table.add_row([hook.get('id'), hook.get('name'), hook.get('uri')]) + + env.fout(table) diff --git a/SoftLayer/CLI/account/invoice_detail.py b/SoftLayer/CLI/account/invoice_detail.py index 1fd7e2d5f..4436c44d9 100644 --- a/SoftLayer/CLI/account/invoice_detail.py +++ b/SoftLayer/CLI/account/invoice_detail.py @@ -3,19 +3,26 @@ import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers.account import AccountManager as AccountManager from SoftLayer import utils -@click.command() +@click.command(cls=SLCommand) @click.argument('identifier') @click.option('--details', is_flag=True, default=False, show_default=True, 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) @@ -48,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([ '>>>', @@ -69,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/account/invoices.py b/SoftLayer/CLI/account/invoices.py index 0e1b2a59f..8f0281ec9 100644 --- a/SoftLayer/CLI/account/invoices.py +++ b/SoftLayer/CLI/account/invoices.py @@ -3,19 +3,20 @@ import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers.account import AccountManager as AccountManager from SoftLayer import utils -@click.command() -@click.option('--limit', default=50, show_default=True, - help="How many invoices to get back.") -@click.option('--closed', is_flag=True, default=False, show_default=True, - help="Include invoices with a CLOSED status.") +@click.command(cls=SLCommand) @click.option('--all', 'get_all', is_flag=True, default=False, show_default=True, help="Return ALL invoices. There may be a lot of these.") +@click.option('--closed', is_flag=True, default=False, show_default=True, + help="Include invoices with a CLOSED status.") +@click.option('--limit', default=50, show_default=True, + help="How many invoices to get back.") @environment.pass_env def cli(env, limit, closed=False, get_all=False): """List invoices""" diff --git a/SoftLayer/CLI/account/item_detail.py b/SoftLayer/CLI/account/item_detail.py index ddc2d31ed..a67ed6f65 100644 --- a/SoftLayer/CLI/account/item_detail.py +++ b/SoftLayer/CLI/account/item_detail.py @@ -2,13 +2,14 @@ # :license: MIT, see LICENSE for more details. import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers.account import AccountManager as AccountManager from SoftLayer import utils -@click.command() +@click.command(cls=SLCommand) @click.argument('identifier') @environment.pass_env def cli(env, identifier): @@ -22,13 +23,13 @@ def item_table(item): """Formats a table for billing items""" date_format = '%Y-%m-%d' - table = formatting.Table(["Key", "Value"], title="{}".format(item.get('description', 'Billing Item'))) + table = formatting.Table(["Key", "Value"], title=f"{item.get('description', 'Billing Item')}") table.add_row(['createDate', utils.clean_time(item.get('createDate'), date_format, date_format)]) table.add_row(['cycleStartDate', utils.clean_time(item.get('cycleStartDate'), date_format, date_format)]) table.add_row(['cancellationDate', utils.clean_time(item.get('cancellationDate'), date_format, date_format)]) table.add_row(['description', item.get('description')]) table.align = 'l' - fqdn = "{}.{}".format(item.get('hostName'), item.get('domain')) + fqdn = f"{item.get('hostName', '')}.{item.get('domainName', '')}" if fqdn != ".": table.add_row(['FQDN', fqdn]) @@ -42,7 +43,7 @@ def item_table(item): ordered_by = "IBM" user = utils.lookup(item, 'orderItem', 'order', 'userRecord') if user: - ordered_by = "{} ({})".format(user.get('displayName'), utils.lookup(user, 'userStatus', 'name')) + ordered_by = f"{user.get('displayName')} ({utils.lookup(user, 'userStatus', 'name')})" table.add_row(['Ordered By', ordered_by]) table.add_row(['Notes', item.get('notes')]) table.add_row(['Location', utils.lookup(item, 'location', 'name')]) diff --git a/SoftLayer/CLI/account/licenses.py b/SoftLayer/CLI/account/licenses.py index 42ddd9a8d..7b328c7dd 100644 --- a/SoftLayer/CLI/account/licenses.py +++ b/SoftLayer/CLI/account/licenses.py @@ -4,12 +4,13 @@ from SoftLayer import utils +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers import account -@click.command() +@click.command(cls=SLCommand) @environment.pass_env def cli(env): """Show all licenses.""" diff --git a/SoftLayer/CLI/account/orders.py b/SoftLayer/CLI/account/orders.py index f1fb5eeb6..f4cc1fa8b 100644 --- a/SoftLayer/CLI/account/orders.py +++ b/SoftLayer/CLI/account/orders.py @@ -3,22 +3,41 @@ import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers.account import AccountManager as AccountManager from SoftLayer import utils -@click.command() +def upgrade_table(upgrades): + """Formats a table for upgrade orders""" + table = formatting.Table(['Id', 'Maintance window', 'Status', 'Created Date', + 'Case'], title="Upgrade orders") + table.align['Subject'] = 'l' + table.align['Impacted Resources'] = 'l' + for upgrade in upgrades: + table.add_row([upgrade.get('id'), + upgrade.get('maintenanceStartTimeUtc'), + upgrade.get('statusId'), + upgrade.get('createDate'), + upgrade.get('ticketId') or '--']) + return table + + +@click.command(cls=SLCommand) @click.option('--limit', '-l', help='How many results to get in one api call', default=100, show_default=True) +@click.option('--upgrades', is_flag=True, default=False, + help="Show upgrades orders.") @environment.pass_env -def cli(env, limit): +def cli(env, limit, upgrades): """Lists account orders. Use `slcli order lookup ` to find more details about a specific order.""" manager = AccountManager(env.client) orders = manager.get_account_all_billing_orders(limit) + upgrade = manager.get_account_upgrade_orders(limit) order_table = formatting.Table(['Id', 'State', 'User', 'Date', 'Amount', 'Item'], title="orders") @@ -33,3 +52,5 @@ def cli(env, limit): order_table.add_row([order['id'], order['status'], order['userRecord']['username'], create_date, order['orderTotalAmount'], utils.trim_to(' '.join(map(str, items)), 50)]) env.fout(order_table) + if upgrades: + env.fout(upgrade_table(upgrade)) diff --git a/SoftLayer/CLI/account/summary.py b/SoftLayer/CLI/account/summary.py index f1ae2b6be..2b4a7106d 100644 --- a/SoftLayer/CLI/account/summary.py +++ b/SoftLayer/CLI/account/summary.py @@ -2,13 +2,14 @@ # :license: MIT, see LICENSE for more details. import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.managers.account import AccountManager as AccountManager from SoftLayer import utils -@click.command() +@click.command(cls=SLCommand) @environment.pass_env def cli(env): """Prints some various bits of information about an account""" diff --git a/SoftLayer/CLI/autoscale/__init__.py b/SoftLayer/CLI/autoscale/__init__.py deleted file mode 100644 index 80cd82747..000000000 --- a/SoftLayer/CLI/autoscale/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Autoscale""" diff --git a/SoftLayer/CLI/autoscale/detail.py b/SoftLayer/CLI/autoscale/detail.py deleted file mode 100644 index 337be43a6..000000000 --- a/SoftLayer/CLI/autoscale/detail.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Get details of an Autoscale groups.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer.managers.autoscale import AutoScaleManager -from SoftLayer import utils - - -@click.command() -@click.argument('identifier') -@environment.pass_env -def cli(env, identifier): - """Get details of an Autoscale groups.""" - - autoscale = AutoScaleManager(env.client) - group = autoscale.details(identifier) - - # Group Config Table - table = formatting.KeyValueTable(["Group", "Value"]) - table.align['Group'] = 'l' - table.align['Value'] = 'l' - - table.add_row(['Id', group.get('id')]) - # Ideally we would use regionalGroup->preferredDatacenter, but that generates an error. - table.add_row(['Datacenter', group['regionalGroup']['locations'][0]['longName']]) - table.add_row(['Termination', utils.lookup(group, 'terminationPolicy', 'name')]) - table.add_row(['Minimum Members', group.get('minimumMemberCount')]) - table.add_row(['Maximum Members', group.get('maximumMemberCount')]) - table.add_row(['Current Members', group.get('virtualGuestMemberCount')]) - table.add_row(['Cooldown', "{} seconds".format(group.get('cooldown'))]) - table.add_row(['Last Action', utils.clean_time(group.get('lastActionDate'))]) - - for network in group.get('networkVlans', []): - network_type = utils.lookup(network, 'networkVlan', 'networkSpace') - router = utils.lookup(network, 'networkVlan', 'primaryRouter', 'hostname') - vlan_number = utils.lookup(network, 'networkVlan', 'vlanNumber') - vlan_name = "{}.{}".format(router, vlan_number) - table.add_row([network_type, vlan_name]) - - env.fout(table) - - # Template Config Table - config_table = formatting.KeyValueTable(["Template", "Value"]) - config_table.align['Template'] = 'l' - config_table.align['Value'] = 'l' - - template = group.get('virtualGuestMemberTemplate') - - config_table.add_row(['Hostname', template.get('hostname')]) - config_table.add_row(['Domain', template.get('domain')]) - config_table.add_row(['Core', template.get('startCpus')]) - config_table.add_row(['Ram', template.get('maxMemory')]) - network = template.get('networkComponents') - config_table.add_row(['Network', network[0]['maxSpeed'] if network else 'Default']) - ssh_keys = template.get('sshKeys', []) - ssh_manager = SoftLayer.SshKeyManager(env.client) - for key in ssh_keys: - # Label isn't included when retrieved from the AutoScale group... - ssh_key = ssh_manager.get_key(key.get('id')) - config_table.add_row(['SSH Key {}'.format(ssh_key.get('id')), ssh_key.get('label')]) - disks = template.get('blockDevices', []) - disk_type = "Local" if template.get('localDiskFlag') else "SAN" - - for disk in disks: - disk_image = disk.get('diskImage') - config_table.add_row(['{} Disk {}'.format(disk_type, disk.get('device')), disk_image.get('capacity')]) - config_table.add_row(['OS', template.get('operatingSystemReferenceCode')]) - config_table.add_row(['Post Install', template.get('postInstallScriptUri') or 'None']) - - env.fout(config_table) - - # Policy Config Table - policy_table = formatting.KeyValueTable(["Policy", "Cooldown"]) - policies = group.get('policies', []) - for policy in policies: - policy_table.add_row([policy.get('name'), policy.get('cooldown') or group.get('cooldown')]) - - env.fout(policy_table) - - # Active Guests - member_table = formatting.Table(['Id', 'Hostname', 'Created'], title="Active Guests") - guests = group.get('virtualGuestMembers', []) - for guest in guests: - real_guest = guest.get('virtualGuest') - member_table.add_row([ - real_guest.get('id'), real_guest.get('hostname'), utils.clean_time(real_guest.get('provisionDate')) - ]) - env.fout(member_table) diff --git a/SoftLayer/CLI/autoscale/edit.py b/SoftLayer/CLI/autoscale/edit.py deleted file mode 100644 index 94e2165af..000000000 --- a/SoftLayer/CLI/autoscale/edit.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Edits an Autoscale group.""" -# :license: MIT, see LICENSE for more details. - -import click - -from SoftLayer.CLI import environment -from SoftLayer.managers.autoscale import AutoScaleManager - - -@click.command() -@click.argument('identifier') -@click.option('--name', help="Scale group's name.") -@click.option('--min', 'minimum', type=click.INT, help="Set the minimum number of guests") -@click.option('--max', 'maximum', type=click.INT, help="Set the maximum number of guests") -@click.option('--userdata', help="User defined metadata string") -@click.option('--userfile', '-F', help="Read userdata from a file", - type=click.Path(exists=True, readable=True, resolve_path=True)) -@click.option('--cpu', type=click.INT, help="Number of CPUs for new guests (existing not effected") -@click.option('--memory', type=click.INT, help="RAM in MB or GB for new guests (existing not effected") -@environment.pass_env -def cli(env, identifier, name, minimum, maximum, userdata, userfile, cpu, memory): - """Edits an Autoscale group.""" - - template = {} - autoscale = AutoScaleManager(env.client) - group = autoscale.details(identifier) - - template['name'] = name - template['minimumMemberCount'] = minimum - template['maximumMemberCount'] = maximum - virt_template = {} - if userdata: - virt_template['userData'] = [{"value": userdata}] - elif userfile: - with open(userfile, 'r', encoding="utf-8") as userfile_obj: - virt_template['userData'] = [{"value": userfile_obj.read()}] - virt_template['startCpus'] = cpu - virt_template['maxMemory'] = memory - - # Remove any entries that are `None` as the API will complain about them. - template['virtualGuestMemberTemplate'] = clean_dict(virt_template) - clean_template = clean_dict(template) - - # If there are any values edited in the template, we need to get the OLD template values and replace them. - if template['virtualGuestMemberTemplate']: - # Update old template with new values - for key, value in clean_template['virtualGuestMemberTemplate'].items(): - group['virtualGuestMemberTemplate'][key] = value - clean_template['virtualGuestMemberTemplate'] = group['virtualGuestMemberTemplate'] - - autoscale.edit(identifier, clean_template) - click.echo("Done") - - -def clean_dict(dictionary): - """Removes any `None` entires from the dictionary""" - return {k: v for k, v in dictionary.items() if v} diff --git a/SoftLayer/CLI/autoscale/list.py b/SoftLayer/CLI/autoscale/list.py deleted file mode 100644 index 2d360714c..000000000 --- a/SoftLayer/CLI/autoscale/list.py +++ /dev/null @@ -1,29 +0,0 @@ -"""List Autoscale groups.""" -# :license: MIT, see LICENSE for more details. - -import click - -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer.managers.autoscale import AutoScaleManager -from SoftLayer import utils - - -@click.command() -@environment.pass_env -def cli(env): - """List AutoScale Groups.""" - - autoscale = AutoScaleManager(env.client) - groups = autoscale.list() - - table = formatting.Table(["Id", "Name", "Status", "Min/Max", "Running"]) - table.align['Name'] = 'l' - for group in groups: - status = utils.lookup(group, 'status', 'name') - min_max = "{}/{}".format(group.get('minimumMemberCount'), group.get('maximumMemberCount')) - table.add_row([ - group.get('id'), group.get('name'), status, min_max, group.get('virtualGuestMemberCount') - ]) - - env.fout(table) diff --git a/SoftLayer/CLI/autoscale/logs.py b/SoftLayer/CLI/autoscale/logs.py deleted file mode 100644 index 6c4c401b3..000000000 --- a/SoftLayer/CLI/autoscale/logs.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Retreive logs for an autoscale group""" -# :license: MIT, see LICENSE for more details. - -import click - -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer.managers.autoscale import AutoScaleManager -from SoftLayer import utils - - -@click.command() -@click.argument('identifier') -@click.option('--date-min', '-d', 'date_min', type=click.DateTime(formats=["%Y-%m-%d", "%m/%d/%Y"]), - help='Earliest date to retreive logs for.') -@environment.pass_env -def cli(env, identifier, date_min): - """Retreive logs for an autoscale group""" - - autoscale = AutoScaleManager(env.client) - - mask = "mask[id,createDate,description]" - object_filter = {} - if date_min: - object_filter['logs'] = { - 'createDate': { - 'operation': 'greaterThanDate', - 'options': [{'name': 'date', 'value': [date_min.strftime("%m/%d/%Y")]}] - } - } - - logs = autoscale.get_logs(identifier, mask=mask, object_filter=object_filter) - table = formatting.Table(['Date', 'Entry'], title="Logs") - table.align['Entry'] = 'l' - for log in logs: - table.add_row([utils.clean_time(log.get('createDate')), log.get('description')]) - env.fout(table) diff --git a/SoftLayer/CLI/autoscale/scale.py b/SoftLayer/CLI/autoscale/scale.py deleted file mode 100644 index 69fe1305a..000000000 --- a/SoftLayer/CLI/autoscale/scale.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Scales an Autoscale group""" -# :license: MIT, see LICENSE for more details. - -import click - -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting -from SoftLayer.managers.autoscale import AutoScaleManager -from SoftLayer import utils - - -@click.command() -@click.argument('identifier') -@click.option('--up/--down', 'scale_up', is_flag=True, default=True, - help="'--up' adds guests, '--down' removes guests.") -@click.option('--by/--to', 'scale_by', is_flag=True, required=True, - help="'--by' will add/remove the specified number of guests." - " '--to' will add/remove a number of guests to get the group's guest count to the specified number.") -@click.option('--amount', required=True, type=click.INT, help="Number of guests for the scale action.") -@environment.pass_env -def cli(env, identifier, scale_up, scale_by, amount): - """Scales an Autoscale group. Bypasses a scale group's cooldown period.""" - - autoscale = AutoScaleManager(env.client) - - # Scale By, and go down, need to use negative amount - if not scale_up and scale_by: - amount = amount * -1 - - result = [] - if scale_by: - click.secho("Scaling group {} by {}".format(identifier, amount), fg='green') - result = autoscale.scale(identifier, amount) - else: - click.secho("Scaling group {} to {}".format(identifier, amount), fg='green') - result = autoscale.scale_to(identifier, amount) - - try: - # Check if the first guest has a cancellation date, assume we are removing guests if it is. - cancel_date = result[0]['virtualGuest']['billingItem']['cancellationDate'] or False - except (IndexError, KeyError, TypeError): - cancel_date = False - - if cancel_date: - member_table = formatting.Table(['Id', 'Hostname', 'Created'], title="Cancelled Guests") - else: - member_table = formatting.Table(['Id', 'Hostname', 'Created'], title="Added Guests") - - for guest in result: - real_guest = guest.get('virtualGuest') - member_table.add_row([ - guest.get('id'), real_guest.get('hostname'), utils.clean_time(real_guest.get('createDate')) - ]) - - env.fout(member_table) diff --git a/SoftLayer/CLI/autoscale/tag.py b/SoftLayer/CLI/autoscale/tag.py deleted file mode 100644 index 58e4101b7..000000000 --- a/SoftLayer/CLI/autoscale/tag.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Tags all guests in an autoscale group.""" -# :license: MIT, see LICENSE for more details. - -import click - -from SoftLayer.CLI import environment -from SoftLayer.managers.autoscale import AutoScaleManager -from SoftLayer.managers.vs import VSManager - - -@click.command() -@click.argument('identifier') -@click.option('--tags', '-g', help="Tags to set for each guest in this group. Existing tags are overwritten. " - "An empty string will remove all tags") -@environment.pass_env -def cli(env, identifier, tags): - """Tags all guests in an autoscale group. - - --tags "Use, quotes, if you, want whitespace" - - --tags Otherwise,Just,commas - """ - - autoscale = AutoScaleManager(env.client) - vsmanager = VSManager(env.client) - mask = "mask[id,virtualGuestId,virtualGuest[tagReferences,id,hostname]]" - guests = autoscale.get_virtual_guests(identifier, mask=mask) - click.echo("New Tags: {}".format(tags)) - for guest in guests: - real_guest = guest.get('virtualGuest') - click.echo("Setting tags for {}".format(real_guest.get('hostname'))) - vsmanager.set_tags(tags, real_guest.get('id'),) - click.echo("Done") diff --git a/SoftLayer/CLI/bandwidth/__init__.py b/SoftLayer/CLI/bandwidth/__init__.py new file mode 100644 index 000000000..a24a16947 --- /dev/null +++ b/SoftLayer/CLI/bandwidth/__init__.py @@ -0,0 +1,2 @@ +"""Bandwidth.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/bandwidth/pools.py b/SoftLayer/CLI/bandwidth/pools.py new file mode 100644 index 000000000..b8bc6be9b --- /dev/null +++ b/SoftLayer/CLI/bandwidth/pools.py @@ -0,0 +1,80 @@ +"""Displays information about the bandwidth pools""" +# :license: MIT, see LICENSE for more details. +import concurrent.futures as cf +import logging +import time + +import click + +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + +LOGGER = logging.getLogger(__name__) + + +@click.command(cls=SLCommand, ) +@environment.pass_env +def cli(env): + """Displays bandwidth pool information + + Similiar to https://cloud.ibm.com/classic-bandwidth/pools + + More information + https://cloud.ibm.com/docs/bandwidth-metering?topic=bandwidth-metering-get-started-with-bandwidth-metering + """ + + manager = AccountManager(env.client) + + items = manager.get_bandwidth_pools() + + table = formatting.Table([ + "Id", + "Name", + "Region", + "Devices", + "Allocation", + "Current Usage", + "Projected Usage", + "Cost", + "Deletion" + ], title="Bandwidth Pools") + table.align = 'l' + + start_m = time.perf_counter() + + with cf.ThreadPoolExecutor(max_workers=5) as executor: + for item, servers in zip(items, executor.map(manager.get_bandwidth_pool_counts, + [item.get('id') for item in items])): + + id_bandwidth = item.get('id') + name = item.get('name') + region = utils.lookup(item, 'locationGroup', 'name') + + allocation = f"{item.get('totalBandwidthAllocated', 0)} GB" + + current = utils.lookup(item, 'billingCyclePublicBandwidthUsage', 'amountOut') + if current is not None: + current = f"{current} GB" + else: + current = "0 GB" + + projected = f"{item.get('projectedPublicBandwidthUsage', 0)} GB" + + cost = utils.lookup(item, 'billingItem', 'nextInvoiceTotalRecurringAmount') + if cost is not None: + cost = f"${cost}" + else: + cost = "$0.0" + + deletion = utils.clean_time(item.get('endDate')) + if deletion == '': + deletion = formatting.blank() + + table.add_row([id_bandwidth, name, region, servers, allocation, current, projected, cost, deletion]) + + end_m = time.perf_counter() + LOGGER.debug('Total API Call time %s', end_m - start_m) + env.fout(table) diff --git a/SoftLayer/CLI/bandwidth/pools_create.py b/SoftLayer/CLI/bandwidth/pools_create.py new file mode 100644 index 000000000..e14aa2bba --- /dev/null +++ b/SoftLayer/CLI/bandwidth/pools_create.py @@ -0,0 +1,82 @@ +"""Create bandwidth pool.""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer import BandwidthManager +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + +location_groups = { + "SJC/DAL/WDC/TOR/MON": "US/Canada", + "AMS/LON/MAD/PAR": "AMS/LON/MAD/PAR", + "SNG/HKG/OSA/TOK": "SNG/HKG/JPN", + "SYD": "AUS", + "MEX": "MEX", + "SAO": "BRA", + "CHE": "IND", + "MIL": "ITA", + "SEO": "KOR", + "FRA": "FRA" +} + +regions = ['SJC/DAL/WDC/TOR/MON', 'AMS/LON/MAD/PAR', 'SNG/HKG/OSA/TOK', 'SYD', 'MEX', 'SAO', 'CHE', 'MIL', 'SEO', 'FRA'] + + +def check_region_param(ctx, param, value): # pylint: disable=unused-argument + """Check if provided region is region group or part of region + + :params string value: Region or Region-Groups + return string Region-Groups + """ + + region_group = None + for key in location_groups: + if value in key or value is key: + region_group = key + + if region_group: + return region_group + else: + raise click.BadParameter(f"{value} is not a region or part of any region." + " Available Choices: ['SJC/DAL/WDC/TOR/MON', 'AMS/LON/MAD/PAR'," + " 'SNG/HKG/OSA/TOK', 'SYD', 'MEX', 'SAO', 'CHE', 'MIL', 'SEO', 'FRA']") + + +@click.command(cls=SLCommand) +@click.option('--name', required=True, help="Pool name") +@click.option('--region', required=True, + help=f"Choose Region/Region-Group {regions}", callback=check_region_param) +@click.help_option('--help', '-h') +@environment.pass_env +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() + id_location_group = get_id_from_location_group(locations, location_groups[region]) + created_pool = manager.create_pool(name, id_location_group) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.add_row(['Id', created_pool.get('id')]) + table.add_row(['Name Pool', name]) + table.add_row(['Region', region]) + table.add_row(['Created Date', utils.clean_time(created_pool.get('createDate'))]) + env.fout(table) + + +def get_id_from_location_group(locations, name): + """Gets the ID location group, from name""" + for location in locations: + if location['name'] == name: + return location['id'] + + return None diff --git a/SoftLayer/CLI/bandwidth/pools_delete.py b/SoftLayer/CLI/bandwidth/pools_delete.py new file mode 100644 index 000000000..778336ef6 --- /dev/null +++ b/SoftLayer/CLI/bandwidth/pools_delete.py @@ -0,0 +1,18 @@ +"""Delete bandwidth pool.""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer import BandwidthManager +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment + + +@click.command(cls=SLCommand) +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Delete bandwidth pool.""" + + manager = BandwidthManager(env.client) + manager.delete_pool(identifier) + env.fout(f"Bandwidth pool {identifier} has been scheduled for deletion.") diff --git a/SoftLayer/CLI/bandwidth/pools_detail.py b/SoftLayer/CLI/bandwidth/pools_detail.py new file mode 100644 index 000000000..c59c63006 --- /dev/null +++ b/SoftLayer/CLI/bandwidth/pools_detail.py @@ -0,0 +1,86 @@ +"""Get bandwidth pool details.""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer import AccountManager +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + + +@click.command(cls=SLCommand) +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Get bandwidth pool details.""" + + manager = AccountManager(env.client) + bandwidths = manager.getBandwidthDetail(identifier) + + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['Id', bandwidths['id']]) + table.add_row(['Name', bandwidths['name']]) + table.add_row(['Create Date', utils.clean_time(bandwidths.get('createDate'), '%Y-%m-%d')]) + end_date = utils.clean_time(bandwidths.get('endDate')) + if end_date == '': + end_date = formatting.blank() + table.add_row(['End Date', end_date]) + else: + table.add_row(['End Date', utils.clean_time(bandwidths.get('endDate'))]) + current = f"{utils.lookup(bandwidths, 'billingCyclePublicBandwidthUsage', 'amountOut')} GB" + if current is None: + current = '-' + table.add_row(['Current Usage', current]) + projected = f"{bandwidths.get('projectedPublicBandwidthUsage', 0)} GB" + if projected is None: + projected = '-' + table.add_row(['Projected Usage', projected]) + inbound = f"{bandwidths.get('inboundPublicBandwidthUsage', 0)} GB" + if inbound is None: + inbound = '-' + table.add_row(['Inbound Usage', inbound]) + if bandwidths['hardware'] != []: + table.add_row(['hardware', *(_bw_table(bandwidths['hardware']))]) + else: + table.add_row(['hardware', 'Not Found']) + + if bandwidths['virtualGuests'] != []: + table.add_row(['virtualGuests', *(_virtual_table(bandwidths['virtualGuests']))]) + else: + table.add_row(['virtualGuests', 'Not Found']) + + if bandwidths['bareMetalInstances'] != []: + table.add_row(['Netscaler', *(_bw_table(bandwidths['bareMetalInstances']))]) + else: + table.add_row(['Netscaler', 'Not Found']) + + env.fout(table) + + +def _bw_table(bw_data): + """Generates a bandwidth useage table""" + table_data = formatting.Table(['Id', 'HostName', "IP Address", 'Amount', "Current Usage"]) + for bw_point in bw_data: + amount = f"{utils.lookup(bw_point, 'bandwidthAllotmentDetail', 'allocation', 'amount')} GB" + current = f"{bw_point.get('outboundBandwidthUsage', 0)} GB" + ip_address = bw_point.get('primaryIpAddress') + if ip_address is None: + ip_address = '-' + table_data.add_row([bw_point['id'], bw_point['fullyQualifiedDomainName'], ip_address, amount, current]) + return [table_data] + + +def _virtual_table(bw_data): + """Generates a virtual bandwidth usage table""" + table_data = formatting.Table(['Id', 'HostName', "IP Address", 'Amount', "Current Usage"]) + for bw_point in bw_data: + amount = f"{utils.lookup(bw_point, 'bandwidthAllotmentDetail', 'allocation', 'amount')} GB" + current = f"{bw_point.get('outboundBandwidthUsage', 0)} GB" + ip_address = bw_point.get('primaryIpAddress') + if ip_address is None: + ip_address = '-' + table_data.add_row([bw_point['id'], bw_point['fullyQualifiedDomainName'], ip_address, amount, current]) + return [table_data] diff --git a/SoftLayer/CLI/bandwidth/pools_edit.py b/SoftLayer/CLI/bandwidth/pools_edit.py new file mode 100644 index 000000000..98f2d08ae --- /dev/null +++ b/SoftLayer/CLI/bandwidth/pools_edit.py @@ -0,0 +1,50 @@ +"""Edit bandwidth pool.""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer import BandwidthManager +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + +location_groups = { + "SJC/DAL/WDC/TOR/MON": "US/Canada", + "AMS/LON/MAD/PAR": "AMS/LON/MAD/PAR", + "SNG/HKG/OSA/TOK": "SNG/HKG/JPN", + "SYD": "AUS", + "MEX": "MEX", + "SAO": "BRA", + "CHE": "IND", + "MIL": "ITA", + "SEO": "KOR", + "FRA": "FRA" +} + + +@click.command(cls=SLCommand) +@click.argument('identifier') +@click.option('--name', required=True, help="Pool name") +@environment.pass_env +def cli(env, identifier, name): + """Edit bandwidth pool.""" + + manager = BandwidthManager(env.client) + bandwidth_pool = manager.edit_pool(identifier, name) + + if bandwidth_pool: + + edited_pool = manager.get_bandwidth_detail(identifier) + locations = manager.get_location_group() + + location = next( + (location for location in locations if location['id'] == edited_pool.get('locationGroupId')), None) + + region_name = next((key for key, value in location_groups.items() if value == location.get('name')), None) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.add_row(['Id', edited_pool.get('id')]) + table.add_row(['Name Pool', name]) + table.add_row(['Region', region_name]) + table.add_row(['Created Date', utils.clean_time(edited_pool.get('createDate'))]) + env.fout(table) diff --git a/SoftLayer/CLI/bandwidth/summary.py b/SoftLayer/CLI/bandwidth/summary.py new file mode 100644 index 000000000..0c752f6ec --- /dev/null +++ b/SoftLayer/CLI/bandwidth/summary.py @@ -0,0 +1,96 @@ +"""Bandwidth summary for every pool/server.""" +import click + +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + + +@click.command(cls=SLCommand, short_help="Bandwidth summary for every pool/server") +@environment.pass_env +def cli(env): + """Bandwidth summary for every pool/server. + + This summary on the total data transfered for each virtual sever, hardware + server and bandwidth pool. + https://cloud.ibm.com/classic-bandwidth + + More information + https://cloud.ibm.com/docs/bandwidth-metering?topic=bandwidth-metering-get-started-with-bandwidth-metering + """ + + table = formatting.Table([ + 'Id', + 'Device name', + 'Location', + 'Allocation', + 'Data in', + 'Data out', + 'Total usage', + 'Pool', + 'Tags', + ]) + + mask = """mask[resource(SoftLayer_Hardware)[id,bandwidthAllocation,bandwidthAllotmentDetail[id,bandwidthAllotment + [id,bandwidthAllotmentTypeId,name]],billingItem[id,createDate,lastBillDate],datacenter[id,name], + fullyQualifiedDomainName,inboundPublicBandwidthUsage,outboundPublicBandwidthUsage,primaryIpAddress,tagReferences + [id,tag[id,name]]],resource(SoftLayer_Network_Application_Delivery_Controller)[id,billingItem[id, + bandwidthAllocation[id,amount],bandwidthAllotmentDetail[id,bandwidthAllotment[id,bandwidthAllotmentTypeId,name]] + ,createDate,lastBillDate],datacenter[id,name],name,outboundPublicBandwidthUsage,primaryIpAddress,tagReferences + [id,tag[id,name]]],resource(SoftLayer_Virtual_Guest)[id,bandwidthAllocation,bandwidthAllotmentDetail[id, + bandwidthAllotment[id,bandwidthAllotmentTypeId,name]],billingItem[id,createDate,lastBillDate],datacenter[id,name + ],fullyQualifiedDomainName,inboundPublicBandwidthUsage,outboundPublicBandwidthUsage,primaryIpAddress, + tagReferences[id,tag[id,name]]]]""" + + search_string = """_objectType:SoftLayer_Hardware,SoftLayer_Virtual_Guest, + SoftLayer_Network_Application_Delivery_Controller _sort:[fullyQualifiedDomainName:asc]""" + + servers = env.client.call( + 'Search', 'advancedSearch', + search_string, + mask=mask, + iter=True + ) + + for server in servers: + resource = server.get('resource') + + device_name = utils.lookup(resource, 'fullyQualifiedDomainName') + if not device_name: + device_name = utils.lookup(resource, 'name') + + bandwidth_allocation = utils.lookup(resource, 'bandwidthAllocation') + if bandwidth_allocation != '0': + if bandwidth_allocation is not None: + bandwidth_allocation = formatting.convert_sizes( + bandwidth_allocation, round_result=True) + else: + bandwidth_allocation = 'Unlimited' + else: + bandwidth_allocation = 'Pay-As-You-Go' + + in_bandwidth_public = formatting.convert_sizes(utils.lookup(resource, 'inboundPublicBandwidthUsage')) + + out_bandwidth_public = formatting.convert_sizes(utils.lookup(resource, 'outboundPublicBandwidthUsage')) + + total_bandwidth_public = formatting.sum_sizes(in_bandwidth_public, out_bandwidth_public) + + if bandwidth_allocation != 'Unlimited' and bandwidth_allocation != 'Pay-As-You-Go': + pool = utils.lookup(resource, 'bandwidthAllotmentDetail', 'bandwidthAllotment', 'name') + else: + pool = 'Not Applicable' + + table.add_row([ + utils.lookup(resource, 'id'), + device_name, + utils.lookup(resource, 'datacenter', 'name'), + bandwidth_allocation, + in_bandwidth_public, + out_bandwidth_public, + total_bandwidth_public, + pool, + formatting.tags(resource.get('tagReferences')), + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/access/authorize.py b/SoftLayer/CLI/block/access/authorize.py index bf2da3af3..4a5931e95 100644 --- a/SoftLayer/CLI/block/access/authorize.py +++ b/SoftLayer/CLI/block/access/authorize.py @@ -6,20 +6,28 @@ from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions +MULTIPLE = '(Multiple allowed)' -@click.command() + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') -@click.option('--hardware-id', '-h', multiple=True, - help='The id of one SoftLayer_Hardware to authorize') -@click.option('--virtual-id', '-v', multiple=True, - help='The id of one SoftLayer_Virtual_Guest to authorize') +@click.option('--hardware-id', '-d', multiple=True, + help='The ID of one hardware server to authorize. ' + MULTIPLE) @click.option('--ip-address-id', '-i', multiple=True, - help='The id of one SoftLayer_Network_Subnet_IpAddress to authorize') + help='The ID of one SoftLayer_Network_Subnet_IpAddress to authorize. ' + MULTIPLE) @click.option('--ip-address', multiple=True, - help='An IP address to authorize') + help='An IP address to authorize. ' + MULTIPLE) +@click.option('--virtual-id', '-v', multiple=True, + help='The ID of one virtual server to authorize. ' + MULTIPLE) @environment.pass_env def cli(env, volume_id, hardware_id, virtual_id, ip_address_id, ip_address): - """Authorizes hosts to access a given volume""" + """Authorize hosts to access a given volume. + + EXAMPLE:: + + slcli block access-authorize 12345678 --virtual-id 87654321 + This command authorizes virtual server with ID 87654321 to access volume with ID 12345678. + """ block_manager = SoftLayer.BlockStorageManager(env.client) ip_address_id_list = list(ip_address_id) diff --git a/SoftLayer/CLI/block/access/list.py b/SoftLayer/CLI/block/access/list.py index 4ff77ef25..c2aa4c243 100644 --- a/SoftLayer/CLI/block/access/list.py +++ b/SoftLayer/CLI/block/access/list.py @@ -10,17 +10,24 @@ from SoftLayer.CLI import storage_utils -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') -@click.option('--sortby', help='Column to sort by', default='name') @click.option('--columns', callback=column_helper.get_formatter(storage_utils.COLUMNS), - help='Columns to display. Options: {0}'.format( - ', '.join(column.name for column in storage_utils.COLUMNS)), + help=f"Columns to display. Options are: {', '.join(column.name for column in storage_utils.COLUMNS)}.", default=','.join(storage_utils.DEFAULT_COLUMNS)) +@click.option('--sortby', + help=f"Column to sort by. Options are: {', '.join(column.name for column in storage_utils.COLUMNS)}.", + default='name') @environment.pass_env def cli(env, columns, sortby, volume_id): - """List ACLs.""" + """List hosts that are authorized to access the volume. + + EXAMPLE:: + + slcli block access-list 12345678 --sortby id + This command lists all hosts that are authorized to access volume with ID 12345678 and sorts them by ID. + """ block_manager = SoftLayer.BlockStorageManager(env.client) resolved_id = helpers.resolve_id(block_manager.resolve_ids, volume_id, 'Volume Id') access_list = block_manager.get_block_volume_access_list( diff --git a/SoftLayer/CLI/block/access/password.py b/SoftLayer/CLI/block/access/password.py index 1046f25d7..744c5c5ab 100644 --- a/SoftLayer/CLI/block/access/password.py +++ b/SoftLayer/CLI/block/access/password.py @@ -6,10 +6,10 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('access_id') @click.option('--password', '-p', multiple=False, - help='Password you want to set, this command will fail if the password is not strong') + help='Password you want to set, this command will fail if the password is not strong.') @environment.pass_env def cli(env, access_id, password): """Changes a password for a volume's access. diff --git a/SoftLayer/CLI/block/access/revoke.py b/SoftLayer/CLI/block/access/revoke.py index c2284beff..d1ab27bd6 100644 --- a/SoftLayer/CLI/block/access/revoke.py +++ b/SoftLayer/CLI/block/access/revoke.py @@ -6,22 +6,25 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') -@click.option('--hardware-id', '-h', multiple=True, - help='The id of one SoftLayer_Hardware' - ' to revoke authorization') -@click.option('--virtual-id', '-v', multiple=True, - help='The id of one SoftLayer_Virtual_Guest' - ' to revoke authorization') +@click.option('--hardware-id', '-d', multiple=True, + help='The ID of one SoftLayer_Hardware to revoke authorization.') @click.option('--ip-address-id', '-i', multiple=True, - help='The id of one SoftLayer_Network_Subnet_IpAddress' - ' to revoke authorization') + help='The ID of one SoftLayer_Network_Subnet_IpAddress to revoke authorization.') @click.option('--ip-address', multiple=True, - help='An IP address to revoke authorization') + help='An IP address to revoke authorization.') +@click.option('--virtual-id', '-v', multiple=True, + help='The ID of one SoftLayer_Virtual_Guest to revoke authorization.') @environment.pass_env def cli(env, volume_id, hardware_id, virtual_id, ip_address_id, ip_address): - """Revokes authorization for hosts accessing a given volume""" + """Revoke authorization for hosts that are accessing a specific volume. + + EXAMPLE:: + + slcli block access-revoke 12345678 --virtual-id 87654321 + This command revokes access of virtual server with ID 87654321 to volume with ID 12345678. + """ block_manager = SoftLayer.BlockStorageManager(env.client) ip_address_id_list = list(ip_address_id) diff --git a/SoftLayer/CLI/block/cancel.py b/SoftLayer/CLI/block/cancel.py index 03118451b..3d25cc27d 100644 --- a/SoftLayer/CLI/block/cancel.py +++ b/SoftLayer/CLI/block/cancel.py @@ -9,21 +9,29 @@ from SoftLayer.CLI import formatting -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') @click.option('--reason', help="An optional reason for cancellation") @click.option('--immediate', is_flag=True, help="Cancels the block storage volume immediately instead " "of on the billing anniversary") +@click.option('--force', default=False, is_flag=True, help="Force cancel block volume without confirmation") @environment.pass_env -def cli(env, volume_id, reason, immediate): - """Cancel an existing block storage volume.""" +def cli(env, volume_id, reason, immediate, force): + """Cancel an existing block storage volume. + + Example:: + slcli block volume-cancel 12345678 --immediate -f + This command cancels volume with ID 12345678 immediately and without asking for confirmation. +""" block_storage_manager = SoftLayer.BlockStorageManager(env.client) - if not (env.skip_confirmations or formatting.no_going_back(volume_id)): - raise exceptions.CLIAbort('Aborted') + if not force: + if not (env.skip_confirmations or + formatting.confirm(f"This will cancel the block volume: {volume_id} and cannot be undone. Continue?")): + raise exceptions.CLIAbort('Aborted') cancelled = block_storage_manager.cancel_block_volume(volume_id, reason, immediate) diff --git a/SoftLayer/CLI/block/convert.py b/SoftLayer/CLI/block/convert.py index a48d8926c..2df175c3f 100644 --- a/SoftLayer/CLI/block/convert.py +++ b/SoftLayer/CLI/block/convert.py @@ -6,7 +6,7 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @environment.pass_env def cli(env, volume_id): diff --git a/SoftLayer/CLI/block/count.py b/SoftLayer/CLI/block/count.py index ecfba0a53..f06c6c935 100644 --- a/SoftLayer/CLI/block/count.py +++ b/SoftLayer/CLI/block/count.py @@ -12,7 +12,7 @@ ] -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.option('--datacenter', '-d', help='Datacenter shortname') @click.option('--sortby', help='Column to sort by', default='Datacenter') @environment.pass_env @@ -30,7 +30,7 @@ def cli(env, sortby, datacenter): service_resource = volume['serviceResource'] if 'datacenter' in service_resource: datacenter_name = service_resource['datacenter']['name'] - if datacenter_name not in datacenters.keys(): + if datacenter_name not in datacenters.keys(): # pylint: disable=consider-iterating-dictionary datacenters[datacenter_name] = 1 else: datacenters[datacenter_name] += 1 diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py index e0cdc8ed1..a4359fae3 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -9,7 +9,7 @@ from SoftLayer import utils -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @environment.pass_env def cli(env, volume_id): @@ -23,83 +23,60 @@ def cli(env, volume_id): table.align['Name'] = 'r' table.align['Value'] = 'l' + capacity = '0' + if block_volume['capacityGb'] != '': + capacity = "%iGB" % block_volume['capacityGb'] storage_type = block_volume['storageType']['keyName'].split('_').pop(0) table.add_row(['ID', block_volume['id']]) table.add_row(['Username', block_volume['username']]) table.add_row(['Type', storage_type]) - table.add_row(['Capacity (GB)', "%iGB" % block_volume['capacityGb']]) - table.add_row(['LUN Id', "%s" % block_volume['lunId']]) + table.add_row(['Capacity (GB)', capacity]) + 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_list = [] + 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(['Replicant ID', - replicant['id']]) - replicant_table.add_row([ - 'Volume Name', - utils.lookup(replicant, 'username')]) replicant_table.add_row([ - 'Target IP', - utils.lookup(replicant, 'serviceResourceBackendIpAddress')]) - replicant_table.add_row([ - 'Data Center', - utils.lookup(replicant, - 'serviceResource', 'datacenter', 'name')]) - replicant_table.add_row([ - 'Schedule', - utils.lookup(replicant, - 'replicationSchedule', 'type', 'keyname')]) - replicant_list.append(replicant_table) - table.add_row(['Replicant Volumes', replicant_list]) + 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']) @@ -110,7 +87,7 @@ def cli(env, volume_id): original_volume_info.add_row(['Original Snapshot Name', block_volume['originalSnapshotName']]) table.add_row(['Original Volume Properties', original_volume_info]) - notes = '{}'.format(block_volume.get('notes', '')) + notes = f"{block_volume.get('notes', '')}" table.add_row(['Notes', notes]) env.fout(table) diff --git a/SoftLayer/CLI/block/duplicate.py b/SoftLayer/CLI/block/duplicate.py index ff9ae961a..44a077bc0 100644 --- a/SoftLayer/CLI/block/duplicate.py +++ b/SoftLayer/CLI/block/duplicate.py @@ -10,7 +10,7 @@ CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()} -@click.command(context_settings=CONTEXT_SETTINGS) +@click.command(cls=SoftLayer.CLI.command.SLCommand, context_settings=CONTEXT_SETTINGS) @click.argument('origin-volume-id') @click.option('--origin-snapshot-id', '-o', type=int, @@ -64,7 +64,13 @@ def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size, duplicate_iops, duplicate_tier, duplicate_snapshot_size, billing, dependent_duplicate): - """Order a duplicate block storage volume.""" + """Order a duplicate block storage volume. + + Example:: + slcli block volume-duplicate 12345678 + This command shows order a new volume by duplicating the volume with ID 12345678. +""" + block_manager = SoftLayer.BlockStorageManager(env.client) hourly_billing_flag = False @@ -89,8 +95,7 @@ def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size, raise exceptions.ArgumentError(str(ex)) if 'placedOrder' in order.keys(): - click.echo("Order #{0} placed successfully!".format( - order['placedOrder']['id'])) + click.echo(f"Order #{order['placedOrder']['id']} placed successfully!") for item in order['placedOrder']['items']: click.echo(" > %s" % item['description']) else: diff --git a/SoftLayer/CLI/block/duplicate_convert_status.py b/SoftLayer/CLI/block/duplicate_convert_status.py new file mode 100644 index 000000000..630e3e1bf --- /dev/null +++ b/SoftLayer/CLI/block/duplicate_convert_status.py @@ -0,0 +1,29 @@ +"""Get status for split or move completed percentage of a given block duplicate volume.""" +# :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('volume-id') +@environment.pass_env +def cli(env, volume_id): + """Get status for split or move completed percentage of a given block storage duplicate volume.""" + table = formatting.Table(['Username', 'Active Conversion Start Timestamp', 'Completed Percentage']) + + block_manager = SoftLayer.BlockStorageManager(env.client) + + value = block_manager.convert_dupe_status(volume_id) + + table.add_row( + [ + value['volumeUsername'], + value['activeConversionStartTime'], + value['deDuplicateConversionPercentage'] + ] + ) + + env.fout(table) diff --git a/SoftLayer/CLI/block/limit.py b/SoftLayer/CLI/block/limit.py index 13d22c8ce..6af71c9e3 100644 --- a/SoftLayer/CLI/block/limit.py +++ b/SoftLayer/CLI/block/limit.py @@ -13,19 +13,38 @@ ] -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.option('--sortby', help='Column to sort by', default='Datacenter') +@click.option('--datacenter', '-d', help='Filter by datacenter') @environment.pass_env -def cli(env, sortby): - """List number of block storage volumes limit per datacenter.""" +def cli(env, sortby, datacenter): + """List number of block storage volumes limit per 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() table = formatting.KeyValueTable(DEFAULT_COLUMNS) table.sortby = sortby - for volume in block_volumes: - datacenter_name = volume['datacenterName'] - maximum_available_count = volume['maximumAvailableCount'] - provisioned_count = volume['provisionedCount'] - table.add_row([datacenter_name, maximum_available_count, provisioned_count]) + + for volumen in block_volumes: + if datacenter: + if volumen.get('datacenterName') != '': + if volumen.get('datacenterName') == datacenter: + table.add_row([volumen.get('datacenterName'), + volumen.get('maximumAvailableCount'), + volumen.get('provisionedCount')]) + break + else: + if volumen.get('datacenterName') != '': + table.add_row([volumen.get('datacenterName'), volumen.get('maximumAvailableCount'), + volumen.get('provisionedCount')]) + else: + table.add_row([' - ', + volumen.get('maximumAvailableCount'), + volumen.get('provisionedCount')]) env.fout(table) diff --git a/SoftLayer/CLI/block/list.py b/SoftLayer/CLI/block/list.py index bd14c7282..9a65603e0 100644 --- a/SoftLayer/CLI/block/list.py +++ b/SoftLayer/CLI/block/list.py @@ -21,7 +21,6 @@ else '-', mask="storageType.keyName"), column_helper.Column('capacity_gb', ('capacityGb',), mask="capacityGb"), - column_helper.Column('bytes_used', ('bytesUsed',), mask="bytesUsed"), column_helper.Column('IOPs', ('provisionedIops',), mask="provisionedIops"), column_helper.Column('ip_addr', ('serviceResourceBackendIpAddress',), mask="serviceResourceBackendIpAddress"), @@ -42,7 +41,6 @@ 'datacenter', 'storage_type', 'capacity_gb', - 'bytes_used', 'IOPs', 'ip_addr', 'lunId', @@ -52,7 +50,7 @@ ] -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.option('--username', '-u', help='Volume username') @click.option('--datacenter', '-d', help='Datacenter shortname') @click.option('--order', '-o', type=int, help='Filter by ID of the order that purchased the block storage') @@ -62,12 +60,18 @@ @click.option('--sortby', help='Column to sort by', default='username') @click.option('--columns', callback=column_helper.get_formatter(COLUMNS), - help='Columns to display. Options: {0}'.format( - ', '.join(column.name for column in COLUMNS)), + help=f"Columns to display. Options: {', '.join(column.name for column in COLUMNS)}", default=','.join(DEFAULT_COLUMNS)) @environment.pass_env def cli(env, sortby, columns, datacenter, username, storage_type, order): - """List block storage.""" + """List block storage. + + Example:: + slcli block volume-list -d dal09 -t endurance --sortby capacity_gb + This command lists all endurance volumes on current account \ +that are located at dal09, and sorts them by capacity. +""" + block_manager = SoftLayer.BlockStorageManager(env.client) block_volumes = block_manager.list_block_volumes(datacenter=datacenter, username=username, diff --git a/SoftLayer/CLI/block/lun.py b/SoftLayer/CLI/block/lun.py index ee33a23b3..3a677fd5a 100644 --- a/SoftLayer/CLI/block/lun.py +++ b/SoftLayer/CLI/block/lun.py @@ -7,7 +7,7 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') @click.argument('lun-id') @environment.pass_env diff --git a/SoftLayer/CLI/block/modify.py b/SoftLayer/CLI/block/modify.py index 3697ddd79..187ff8d7e 100644 --- a/SoftLayer/CLI/block/modify.py +++ b/SoftLayer/CLI/block/modify.py @@ -10,7 +10,7 @@ CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()} -@click.command(context_settings=CONTEXT_SETTINGS) +@click.command(cls=SoftLayer.CLI.command.SLCommand, context_settings=CONTEXT_SETTINGS) @click.argument('volume-id') @click.option('--new-size', '-c', type=int, @@ -20,20 +20,28 @@ @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. + + slcli block volume-modify 12345678 --new-size 500 --new-tier 4 + This command modify a volume 12345678 with size is 500GB, tier level is 4 IOPS per GB. +""" + block_manager = SoftLayer.BlockStorageManager(env.client) if new_tier is not None: @@ -50,7 +58,7 @@ def cli(env, volume_id, new_size, new_iops, new_tier): raise exceptions.ArgumentError(str(ex)) if 'placedOrder' in order.keys(): - click.echo("Order #{0} placed successfully!".format(order['placedOrder']['id'])) + click.echo(f"Order #{order['placedOrder']['id']} placed successfully!") for item in order['placedOrder']['items']: click.echo(" > %s" % item['description']) else: diff --git a/SoftLayer/CLI/block/object_list.py b/SoftLayer/CLI/block/object_list.py new file mode 100644 index 000000000..dd6779294 --- /dev/null +++ b/SoftLayer/CLI/block/object_list.py @@ -0,0 +1,31 @@ +"""List cloud object storage volumes.""" +# :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, ) +@environment.pass_env +def cli(env): + """List cloud block storage.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + + storages = block_manager.get_cloud_list() + + table = formatting.Table(['Id', + 'Account name', + 'Description', + 'Create Date', + 'Type']) + for storage in storages: + table.add_row([storage.get('id'), + storage.get('username'), + storage['storageType']['description'], + storage['billingItem']['createDate'], + storage['storageType']['keyName']]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/object_storage_detail.py b/SoftLayer/CLI/block/object_storage_detail.py new file mode 100644 index 000000000..e7aa69135 --- /dev/null +++ b/SoftLayer/CLI/block/object_storage_detail.py @@ -0,0 +1,34 @@ +"""Display details for a specified cloud object storage.""" +# :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('object_id') +@environment.pass_env +def cli(env, object_id): + """Display details for a cloud object storage.""" + + block_manager = SoftLayer.BlockStorageManager(env.client) + + cloud = block_manager.get_volume_details(object_id) + bucket = block_manager.get_buckets(object_id) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + table.add_row(['Id', cloud.get('id')]) + table.add_row(['Username', cloud.get('username')]) + table.add_row(['Name Service Resource', cloud['serviceResource']['name']]) + table.add_row(['Type Service Resource', cloud['serviceResource']['type']['type']]) + table.add_row(['Datacenter', cloud['serviceResource']['datacenter']['name']]) + table.add_row(['Storage type', cloud['storageType']['keyName']]) + table.add_row(['Bytes Used', formatting.b_to_gb(bucket[0]['bytesUsed'])]) + table.add_row(['Bucket name', bucket[0]['name']]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/object_storage_permission.py b/SoftLayer/CLI/block/object_storage_permission.py new file mode 100644 index 000000000..85d5aae5d --- /dev/null +++ b/SoftLayer/CLI/block/object_storage_permission.py @@ -0,0 +1,44 @@ +"""Display permission details for a cloud object storage.""" +# :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('object_id') +@environment.pass_env +def cli(env, object_id): + """Display permission details for a cloud object storage.""" + + block_manager = SoftLayer.BlockStorageManager(env.client) + + cloud = block_manager.get_network_message_delivery_accounts(object_id) + end_points = block_manager.get_end_points(object_id) + + table = formatting.Table(['Name', 'Value']) + + table_credentials = formatting.Table(['Id', 'Access Key ID', 'Secret Access Key', 'Description']) + + for credential in cloud.get('credentials'): + table_credentials.add_row([credential.get('id'), + credential.get('username'), + credential.get('password'), + credential['type']['description']]) + + table_url = formatting.Table(['Region', + 'Location', + 'Type', + 'URL']) + for end_point in end_points: + table_url.add_row([end_point.get('region') or '', + end_point.get('location') or '', + end_point.get('type'), + end_point.get('url'), ]) + + table.add_row(['UUID', cloud.get('uuid')]) + table.add_row(['Credentials', table_credentials]) + table.add_row(['EndPoint URL´s', table_url]) + env.fout(table) diff --git a/SoftLayer/CLI/block/options.py b/SoftLayer/CLI/block/options.py new file mode 100644 index 000000000..fbd2d7f1b --- /dev/null +++ b/SoftLayer/CLI/block/options.py @@ -0,0 +1,132 @@ +"""List all options for ordering a block storage.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI.command import SLCommand +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + +PACKAGE_STORAGE = 759 + + +@click.command(cls=SLCommand) +@click.argument('location', required=False) +@click.option('--prices', '-p', is_flag=True, + help='Use --prices to list the server item prices, and to list the Item Prices by location,' + 'add it to the --prices option using location short name, e.g. --prices dal13') +@environment.pass_env +def cli(env, prices, location=None): + """List all options for ordering a block storage + + Example:: + slcli block volume-options + This command lists all options for creating a \ +block storage volume, including storage type, volume size, OS type, IOPS, tier level, datacenter, and snapshot size. + + slcli block volume-options --prices + This command lists all options for creating a \ +block storage volume, including storage type, volume size, OS type, IOPS, tier level, \ +datacenter, and snapshot size along with prices. + + slcli block volume-options --prices dal03 + This command lists all options for creating a \ +block storage volume, including storage type, volume size, \ +OS type, IOPS, tier level, datacenter, and snapshot size along with prices for a given location. +""" + + order_manager = SoftLayer.OrderingManager(env.client) + items = order_manager.get_items(PACKAGE_STORAGE, mask="mask[categories]") + datacenters = order_manager.get_regions(PACKAGE_STORAGE, location) + + tables = [] + network = SoftLayer.NetworkManager(env.client) + pods = network.get_closed_pods() + + if datacenters != []: + datacenter_table = formatting.Table(['Id', 'Description', 'KeyName'], title='Datacenter') + + for datacenter in datacenters: + closure = [] + for pod in pods: + if datacenter['location']['location']['name'] in str(pod['name']): + closure.append(pod['name']) + + notes = '-' + if len(closure) > 0: + notes = 'closed soon: %s' % (', '.join(closure)) + datacenter_table.add_row([datacenter['location']['locationId'], + datacenter.get('description'), + datacenter['keyname'], notes]) + tables.append(datacenter_table) + else: + raise exceptions.CLIAbort('Location does not exit.') + + tables.append(_block_ios_get_table(items, prices)) + tables.append(_block_storage_table(items, prices)) + tables.append(_block_snapshot_get_table(items, prices)) + env.fout(tables) + + +def _block_ios_get_table(items, prices): + if prices: + table = formatting.Table(['Id', 'Description', 'KeyName', 'Prices'], title='IOPS') + for block_item in items: + if block_item['itemCategory']['categoryCode'] == 'storage_tier_level': + table.add_row([block_item.get('id'), block_item.get('description'), + block_item.get('keyName'), block_item['prices'][0]['recurringFee']]) + else: + table = formatting.Table(['Id', 'Description', 'KeyName'], title='IOPS') + for block_item in items: + if block_item['itemCategory']['categoryCode'] == 'storage_tier_level': + table.add_row([block_item.get('id'), block_item.get('description'), + block_item.get('keyName')]) + table.sortby = 'KeyName' + table.align = 'l' + return table + + +def _block_storage_table(items, prices): + if prices: + table = formatting.Table(['Id', 'Description', 'KeyName', 'Capacity Minimum', 'Prices'], title='Storage') + for block_item in items: + if block_item['itemCategory']['categoryCode'] == 'performance_storage_space': + table.add_row([block_item.get('id'), block_item.get('description'), + block_item.get('keyName'), block_item.get('capacityMinimum') or '-', + block_item['prices'][0]['recurringFee']]) + else: + table = formatting.Table(['Id', 'Description', 'KeyName', 'Capacity Minimum'], title='Storage') + for block_item in items: + if block_item['itemCategory']['categoryCode'] == 'performance_storage_space': + table.add_row([block_item.get('id'), block_item.get('description'), + block_item.get('keyName'), block_item.get('capacityMinimum') or '-', ]) + table.sortby = 'KeyName' + table.align = 'l' + return table + + +def _block_snapshot_get_table(items, prices): + if prices: + table = formatting.Table(['Id', 'Description', 'KeyName', 'Prices'], title='Snapshot') + for block_item in items: + if block_item['itemCategory']['categoryCode'] == 'storage_snapshot_space': + table.add_row([block_item.get('id'), block_item.get('description'), + block_item.get('keyName'), block_item['prices'][0]['recurringFee']]) + else: + table = formatting.Table(['Id', 'Description', 'KeyName'], title='Snapshot') + for block_item in items: + if is_snapshot_category(block_item.get('categories', [])): + table.add_row([block_item.get('id'), block_item.get('description'), block_item.get('keyName')]) + table.sortby = 'KeyName' + table.align = 'l' + return table + + +def is_snapshot_category(categories): + """Checks if storage_snapshot_space is one of the categories""" + for item in categories: + if item.get('categoryCode') == "storage_snapshot_space": + return True + return False diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py index fa7c6bcf6..bdd0100e0 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -9,7 +9,7 @@ CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()} -@click.command(context_settings=CONTEXT_SETTINGS) +@click.command(cls=SoftLayer.CLI.command.SLCommand, context_settings=CONTEXT_SETTINGS) @click.option('--storage-type', help='Type of block storage volume', type=click.Choice(['performance', 'endurance']), @@ -72,12 +72,13 @@ def cli(env, storage_type, size, iops, tier, os_type, hourly_billing_flag = True if service_offering != 'storage_as_a_service': - click.secho('{} is a legacy storage offering'.format(service_offering), fg='red') + click.secho(f"{service_offering} is a legacy storage offering", fg='red') if hourly_billing_flag: raise exceptions.CLIAbort( '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') @@ -123,12 +124,10 @@ def cli(env, storage_type, size, iops, tier, os_type, raise exceptions.ArgumentError(str(ex)) if 'placedOrder' in order.keys(): - click.echo("Order #{0} placed successfully!".format( - order['placedOrder']['id'])) + click.echo(f"Order #{order['placedOrder']['id']} placed successfully!") for item in order['placedOrder']['items']: click.echo(" > %s" % item['description']) - click.echo( - '\nYou may run "slcli block volume-list --order {0}" to find this block volume after it ' - 'is ready.'.format(order['placedOrder']['id'])) + click.echo(f"\nYou may run 'slcli block volume-list --order {order['placedOrder']['id']}' " + "to find this block volume after it is ready.") else: click.echo("Order could not be placed! Please verify your options and try again.") diff --git a/SoftLayer/CLI/block/refresh.py b/SoftLayer/CLI/block/refresh.py index 369a6c815..e58fbf28d 100644 --- a/SoftLayer/CLI/block/refresh.py +++ b/SoftLayer/CLI/block/refresh.py @@ -6,13 +6,21 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @click.argument('snapshot_id') +@click.option('--force-refresh', '-f', is_flag=True, default=False, show_default=True, + help="Cancel current refresh process and initiates the new refresh.") @environment.pass_env -def cli(env, volume_id, snapshot_id): - """Refresh a duplicate volume with a snapshot from its parent.""" +def cli(env, volume_id, snapshot_id, force_refresh): + """Refresh a duplicate volume with a snapshot from its parent. + + EXAMPLE:: + + slcli block volume-refresh VOLUME_ID SNAPSHOT_ID + Refresh a duplicate VOLUME_ID with a snapshot from its parent SNAPSHOT_ID. + """ block_manager = SoftLayer.BlockStorageManager(env.client) - resp = block_manager.refresh_dupe(volume_id, snapshot_id) + resp = block_manager.refresh_dupe(volume_id, snapshot_id, force_refresh) click.echo(resp) diff --git a/SoftLayer/CLI/block/replication/disaster_recovery_failover.py b/SoftLayer/CLI/block/replication/disaster_recovery_failover.py index 1a0c304d8..000263525 100644 --- a/SoftLayer/CLI/block/replication/disaster_recovery_failover.py +++ b/SoftLayer/CLI/block/replication/disaster_recovery_failover.py @@ -8,16 +8,23 @@ from SoftLayer.CLI import formatting -@click.command(epilog="""Failover an inaccessible block volume to its available replicant volume. -If a volume (with replication) becomes inaccessible due to a disaster event, this method can be used to immediately +@click.command(cls=SoftLayer.CLI.command.SLCommand, + epilog="""If a volume (with replication) becomes inaccessible due to a disaster event, +this method can be used to immediately failover to an available replica in another location. This method does not allow for failback via API. After using this method, to failback to the original volume, please open a support ticket. If you wish to test failover, please use replica-failover.""") @click.argument('volume-id') -@click.option('--replicant-id', help="ID of the replicant volume") +@click.option('--replicant-id', help="ID of the replicant volume.") @environment.pass_env def cli(env, volume_id, replicant_id): - """Failover an inaccessible block volume to its available replicant volume.""" + """Failover an inaccessible block volume to its available replicant volume. + + EXAMPLE:: + + slcli block disaster-recovery-failover 12345678 87654321 + This command performs failover operation for volume with ID 12345678 to replica volume with ID 87654321. + """ block_storage_manager = SoftLayer.BlockStorageManager(env.client) click.secho("""WARNING : Failover an inaccessible block volume to its available replicant volume.""" diff --git a/SoftLayer/CLI/block/replication/failback.py b/SoftLayer/CLI/block/replication/failback.py index 88ab6d627..e7f4dc737 100644 --- a/SoftLayer/CLI/block/replication/failback.py +++ b/SoftLayer/CLI/block/replication/failback.py @@ -6,11 +6,11 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') @environment.pass_env def cli(env, volume_id): - """Failback a block volume from the given replicant volume.""" + """Failback a block volume from the given replica volume.""" block_storage_manager = SoftLayer.BlockStorageManager(env.client) success = block_storage_manager.failback_from_replicant(volume_id) diff --git a/SoftLayer/CLI/block/replication/failover.py b/SoftLayer/CLI/block/replication/failover.py index cd36b2271..1bb63f84f 100644 --- a/SoftLayer/CLI/block/replication/failover.py +++ b/SoftLayer/CLI/block/replication/failover.py @@ -6,12 +6,12 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') -@click.option('--replicant-id', help="ID of the replicant volume") +@click.option('--replicant-id', help="ID of the replicant volume.") @environment.pass_env def cli(env, volume_id, replicant_id): - """Failover a block volume to the given replicant volume.""" + """Failover a block volume to the given replica volume.""" block_storage_manager = SoftLayer.BlockStorageManager(env.client) success = block_storage_manager.failover_to_replicant( diff --git a/SoftLayer/CLI/block/replication/locations.py b/SoftLayer/CLI/block/replication/locations.py index 80ba5d98b..0767d0811 100644 --- a/SoftLayer/CLI/block/replication/locations.py +++ b/SoftLayer/CLI/block/replication/locations.py @@ -20,14 +20,13 @@ ] -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') -@click.option('--sortby', help='Column to sort by', default='Long Name') @click.option('--columns', callback=column_helper.get_formatter(COLUMNS), - help='Columns to display. Options: {0}'.format( - ', '.join(column.name for column in COLUMNS)), + help=f"Columns to display. Options: {', '.join(column.name for column in COLUMNS)}", default=','.join(DEFAULT_COLUMNS)) +@click.option('--sortby', help='Column to sort by', default='Long Name') @environment.pass_env def cli(env, columns, sortby, volume_id): """List suitable replication datacenters for the given volume.""" diff --git a/SoftLayer/CLI/block/replication/order.py b/SoftLayer/CLI/block/replication/order.py index 5324dfc19..e3c666f2a 100644 --- a/SoftLayer/CLI/block/replication/order.py +++ b/SoftLayer/CLI/block/replication/order.py @@ -12,24 +12,18 @@ CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()} -@click.command(context_settings=CONTEXT_SETTINGS) +@click.command(cls=SoftLayer.CLI.command.SLCommand, context_settings=CONTEXT_SETTINGS) @click.argument('volume_id') -@click.option('--snapshot-schedule', '-s', - help='Snapshot schedule to use for replication, ' - '(HOURLY | DAILY | WEEKLY)', - required=True, - type=click.Choice(['HOURLY', 'DAILY', 'WEEKLY'])) -@click.option('--location', '-l', - help='Short name of the data center for the replicant ' - '(e.g.: dal09)', +@click.option('--datacenter', '-d', + help='Short name of the datacenter for the replica (e.g.: dal09)', required=True) -@click.option('--tier', - help='Endurance Storage Tier (IOPS per GB) of the primary' - ' volume for which a replicant is ordered [optional]', - type=click.Choice(['0.25', '2', '4', '10'])) -@click.option('--os-type', - help='Operating System Type (e.g.: LINUX) of the primary' - ' volume for which a replica is ordered [optional]', +@click.option('--iops', '-i', + help='Performance Storage IOPs, between 100 and 6000 in multiples of 100. If no IOPS value is specified,' + ' the IOPS value of the original volume will be used.', + type=int) +@click.option('--os-type', '-o', + help='Operating System Type (eg. LINUX) of the primary volume for ' + 'which a replica is ordered [optional].', type=click.Choice([ 'HYPER_V', 'LINUX', @@ -38,8 +32,17 @@ 'WINDOWS_GPT', 'WINDOWS', 'XEN'])) +@click.option('--snapshot-schedule', '-s', + help='Snapshot schedule to use for replication. Options are: ' + 'HOURLY, DAILY, WEEKLY', + required=True, + type=click.Choice(['HOURLY', 'DAILY', 'WEEKLY'])) +@click.option('--tier', '-t', + help='Endurance Storage Tier (IOPS per GB) of the primary volume for which a replica is ordered ' + '[optional]. If no tier is specified, the tier of the original volume will be used', + type=click.Choice(['0.25', '2', '4', '10'])) @environment.pass_env -def cli(env, volume_id, snapshot_schedule, location, tier, os_type): +def cli(env, volume_id, snapshot_schedule, datacenter, tier, os_type, iops): """Order a block storage replica volume.""" block_manager = SoftLayer.BlockStorageManager(env.client) block_volume_id = helpers.resolve_id(block_manager.resolve_ids, volume_id, 'Block Volume') @@ -47,20 +50,27 @@ def cli(env, volume_id, snapshot_schedule, location, tier, os_type): if tier is not None: tier = float(tier) + if iops is not None: + if iops < 100 or iops > 6000: + raise exceptions.ArgumentError(f"Invalid value for '--iops' / '-i': '{iops}' is not one " + "of between 100 and 6000.") + if iops % 100 != 0: + raise exceptions.ArgumentError(f"Invalid value for '--iops' / '-i': '{iops}' is not a multiple of 100.") + try: order = block_manager.order_replicant_volume( block_volume_id, snapshot_schedule=snapshot_schedule, - location=location, + location=datacenter, tier=tier, os_type=os_type, + iops=iops ) except ValueError as ex: raise exceptions.ArgumentError(str(ex)) if 'placedOrder' in order.keys(): - click.echo("Order #{0} placed successfully!".format( - utils.lookup(order, 'placedOrder', 'id'))) + click.echo(f"Order #{utils.lookup(order, 'placedOrder', 'id')} placed successfully!") for item in utils.lookup(order, 'placedOrder', 'items'): click.echo(" > %s" % item.get('description')) else: diff --git a/SoftLayer/CLI/block/replication/partners.py b/SoftLayer/CLI/block/replication/partners.py index ade8f6b0f..c5ae5075b 100644 --- a/SoftLayer/CLI/block/replication/partners.py +++ b/SoftLayer/CLI/block/replication/partners.py @@ -12,17 +12,16 @@ DEFAULT_COLUMNS = storage_utils.REPLICATION_PARTNER_DEFAULT -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') -@click.option('--sortby', help='Column to sort by', default='Username') @click.option('--columns', callback=column_helper.get_formatter(COLUMNS), - help='Columns to display. Options: {0}'.format( - ', '.join(column.name for column in COLUMNS)), + help=f"Columns to display. Options: {', '.join(column.name for column in COLUMNS)}", default=','.join(DEFAULT_COLUMNS)) +@click.option('--sortby', help='Column to sort by', default='Username') @environment.pass_env def cli(env, columns, sortby, volume_id): - """List existing replicant volumes for a block volume.""" + """List existing replica volumes for a block volume.""" block_storage_manager = SoftLayer.BlockStorageManager(env.client) legal_volumes = block_storage_manager.get_replication_partners( diff --git a/SoftLayer/CLI/block/set_note.py b/SoftLayer/CLI/block/set_note.py index eeef42e8e..8c35762ca 100644 --- a/SoftLayer/CLI/block/set_note.py +++ b/SoftLayer/CLI/block/set_note.py @@ -7,7 +7,7 @@ from SoftLayer.CLI import helpers -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') @click.option('--note', '-n', type=str, @@ -15,7 +15,12 @@ help='Public notes related to a Storage volume') @environment.pass_env def cli(env, volume_id, note): - """Set note for an existing block storage volume.""" + """Set note for an existing block storage volume. + + EXAMPLE:: + + slcli block volume-set-note 12345678 --note 'this is my note' + """ block_manager = SoftLayer.BlockStorageManager(env.client) block_volume_id = helpers.resolve_id(block_manager.resolve_ids, volume_id, 'Block Volume') diff --git a/SoftLayer/CLI/block/snapshot/cancel.py b/SoftLayer/CLI/block/snapshot/cancel.py index 7a6150bfe..575bbac04 100644 --- a/SoftLayer/CLI/block/snapshot/cancel.py +++ b/SoftLayer/CLI/block/snapshot/cancel.py @@ -9,15 +9,16 @@ from SoftLayer.CLI import formatting -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume-id') -@click.option('--reason', help="An optional reason for cancellation") +@click.option('--reason', help="An optional reason for cancellation.") @click.option('--immediate', is_flag=True, help="Cancels the snapshot space immediately instead " - "of on the billing anniversary") + "of on the billing anniversary.") +@click.option('--force', default=False, is_flag=True, help="Force modify") @environment.pass_env -def cli(env, volume_id, reason, immediate): +def cli(env, volume_id, reason, immediate, force): """Cancel existing snapshot space for a given volume.""" block_storage_manager = SoftLayer.BlockStorageManager(env.client) @@ -25,6 +26,11 @@ def cli(env, volume_id, reason, immediate): if not (env.skip_confirmations or formatting.no_going_back(volume_id)): raise exceptions.CLIAbort('Aborted') + if not force: + if not (env.skip_confirmations or + formatting.confirm("This action will incur charges on your account. Continue?")): + raise exceptions.CLIAbort('Aborted') + cancelled = block_storage_manager.cancel_snapshot_space( volume_id, reason, immediate) diff --git a/SoftLayer/CLI/block/snapshot/create.py b/SoftLayer/CLI/block/snapshot/create.py index 68dc976fe..92dbe4057 100644 --- a/SoftLayer/CLI/block/snapshot/create.py +++ b/SoftLayer/CLI/block/snapshot/create.py @@ -6,13 +6,13 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @click.option('--notes', '-n', - help='Notes to set on the new snapshot') + help='Notes to set on the new snapshot.') @environment.pass_env def cli(env, volume_id, notes): - """Creates a snapshot on a given volume""" + """Creates a snapshot on a given volume.""" block_manager = SoftLayer.BlockStorageManager(env.client) snapshot = block_manager.create_snapshot(volume_id, notes=notes) diff --git a/SoftLayer/CLI/block/snapshot/delete.py b/SoftLayer/CLI/block/snapshot/delete.py index 229a5d7ef..4c08c59a7 100644 --- a/SoftLayer/CLI/block/snapshot/delete.py +++ b/SoftLayer/CLI/block/snapshot/delete.py @@ -6,11 +6,11 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('snapshot_id') @environment.pass_env def cli(env, snapshot_id): - """Deletes a snapshot on a given volume""" + """Deletes a snapshot on a given volume.""" block_manager = SoftLayer.BlockStorageManager(env.client) deleted = block_manager.delete_snapshot(snapshot_id) diff --git a/SoftLayer/CLI/block/snapshot/disable.py b/SoftLayer/CLI/block/snapshot/disable.py index 0d776bc7e..254b5bd1a 100644 --- a/SoftLayer/CLI/block/snapshot/disable.py +++ b/SoftLayer/CLI/block/snapshot/disable.py @@ -7,14 +7,14 @@ from SoftLayer.CLI import exceptions -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @click.option('--schedule-type', - help='Snapshot schedule [INTERVAL|HOURLY|DAILY|WEEKLY]', + help='Snapshot schedule [INTERVAL|HOURLY|DAILY|WEEKLY].', required=True) @environment.pass_env def cli(env, volume_id, schedule_type): - """Disables snapshots on the specified schedule for a given volume""" + """Disables snapshots on the specified schedule for a given volume.""" if (schedule_type not in ['INTERVAL', 'HOURLY', 'DAILY', 'WEEKLY']): raise exceptions.CLIAbort( diff --git a/SoftLayer/CLI/block/snapshot/enable.py b/SoftLayer/CLI/block/snapshot/enable.py index e81443e57..ec54aa0c0 100644 --- a/SoftLayer/CLI/block/snapshot/enable.py +++ b/SoftLayer/CLI/block/snapshot/enable.py @@ -7,27 +7,27 @@ from SoftLayer.CLI import exceptions -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @click.option('--schedule-type', - help='Snapshot schedule [INTERVAL|HOURLY|DAILY|WEEKLY]', + help='Snapshot schedule [INTERVAL|HOURLY|DAILY|WEEKLY].', required=True) @click.option('--retention-count', - help='Number of snapshots to retain', + help='Number of snapshots to retain.', required=True) @click.option('--minute', - help='Minute of the day when snapshots should be taken', + help='Minute of the hour when snapshots should be taken, integer between 0 to 59.', default=0) @click.option('--hour', - help='Hour of the day when snapshots should be taken', + help='Hour of the day when snapshots should be taken, integer between 0 to 23.', default=0) @click.option('--day-of-week', - help='Day of the week when snapshots should be taken', + help='Day of the week when snapshots should be taken, integer between 0 to 6', default='SUNDAY') @environment.pass_env def cli(env, volume_id, schedule_type, retention_count, minute, hour, day_of_week): - """Enables snapshots for a given volume on the specified schedule""" + """Enables snapshots for a given volume on the specified schedule.""" block_manager = SoftLayer.BlockStorageManager(env.client) valid_schedule_types = {'INTERVAL', 'HOURLY', 'DAILY', 'WEEKLY'} diff --git a/SoftLayer/CLI/block/snapshot/get_notify_status.py b/SoftLayer/CLI/block/snapshot/get_notify_status.py new file mode 100644 index 000000000..9c21085b9 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/get_notify_status.py @@ -0,0 +1,20 @@ +"""Get the snapshots space usage threshold warning flag setting for specific volume""" +# :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('volume_id') +@environment.pass_env +def cli(env, volume_id): + """Get snapshots space usage threshold warning flag setting for a given volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + enabled = block_manager.get_volume_snapshot_notification_status(volume_id) + + if enabled == 0: + click.echo(f"Disabled: Snapshots space usage threshold is disabled for volume {volume_id}") + else: + click.echo(f"Enabled: Snapshots space usage threshold is enabled for volume {volume_id}") diff --git a/SoftLayer/CLI/block/snapshot/list.py b/SoftLayer/CLI/block/snapshot/list.py index 1c2f5c7f8..e9eca5983 100644 --- a/SoftLayer/CLI/block/snapshot/list.py +++ b/SoftLayer/CLI/block/snapshot/list.py @@ -26,14 +26,13 @@ ] -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') -@click.option('--sortby', help='Column to sort by', +@click.option('--sortby', help='Column to sort by.', default='created') @click.option('--columns', callback=column_helper.get_formatter(COLUMNS), - help='Columns to display. Options: {0}'.format( - ', '.join(column.name for column in COLUMNS)), + help=f"Columns to display. Options: {', '.join(column.name for column in COLUMNS)}", default=','.join(DEFAULT_COLUMNS)) @environment.pass_env def cli(env, volume_id, sortby, columns): diff --git a/SoftLayer/CLI/block/snapshot/order.py b/SoftLayer/CLI/block/snapshot/order.py index c5d798eba..a443c95ef 100644 --- a/SoftLayer/CLI/block/snapshot/order.py +++ b/SoftLayer/CLI/block/snapshot/order.py @@ -7,43 +7,53 @@ from SoftLayer.CLI import exceptions -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') -@click.option('--capacity', +@click.option('--iops', type=int, - help='Size of snapshot space to create in GB', + help='Performance Storage IOPs, between 100 and 6000 in multiples of 100.') +@click.option('--size', + type=int, + help='Size of snapshot space to create in GB.', required=True) @click.option('--tier', help='Endurance Storage Tier (IOPS per GB) of the block' ' volume for which space is ordered [optional, and only' - ' valid for endurance storage volumes]', + ' valid for endurance storage volumes].', type=click.Choice(['0.25', '2', '4', '10'])) @click.option('--upgrade', type=bool, - help='Flag to indicate that the order is an upgrade', + help='Flag to indicate that the order is an upgrade.', default=False, is_flag=True) @environment.pass_env -def cli(env, volume_id, capacity, tier, upgrade): +def cli(env, volume_id, size, tier, upgrade, iops): """Order snapshot space for a block storage volume.""" block_manager = SoftLayer.BlockStorageManager(env.client) if tier is not None: tier = float(tier) + if iops is not None: + if iops < 100 or iops > 6000: + raise exceptions.ArgumentError(f"Invalid value for '--iops' / '-i': '{iops}' is not one " + "of between 100 and 6000.") + if iops % 100 != 0: + raise exceptions.ArgumentError(f"Invalid value for '--iops' / '-i': '{iops}' is not a multiple of 100.") + try: order = block_manager.order_snapshot_space( volume_id, - capacity=capacity, + capacity=size, tier=tier, - upgrade=upgrade + upgrade=upgrade, + iops=iops ) except ValueError as ex: raise exceptions.ArgumentError(str(ex)) if 'placedOrder' in order.keys(): - click.echo("Order #{0} placed successfully!".format( - order['placedOrder']['id'])) + click.echo(f"Order #{order['placedOrder']['id']} placed successfully!") for item in order['placedOrder']['items']: click.echo(" > %s" % item['description']) if 'status' in order['placedOrder'].keys(): diff --git a/SoftLayer/CLI/block/snapshot/restore.py b/SoftLayer/CLI/block/snapshot/restore.py index 8cdab7b0a..6cfc5591f 100644 --- a/SoftLayer/CLI/block/snapshot/restore.py +++ b/SoftLayer/CLI/block/snapshot/restore.py @@ -6,14 +6,14 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @click.option('--snapshot-id', '-s', help='The id of the snapshot which will be used' - ' to restore the block volume') + ' to restore the block volume.') @environment.pass_env def cli(env, volume_id, snapshot_id): - """Restore block volume using a given snapshot""" + """Restore block volume using a given snapshot.""" block_manager = SoftLayer.BlockStorageManager(env.client) success = block_manager.restore_from_snapshot(volume_id, snapshot_id) diff --git a/SoftLayer/CLI/block/snapshot/schedule_list.py b/SoftLayer/CLI/block/snapshot/schedule_list.py index 022427e11..a6c6ff2ec 100644 --- a/SoftLayer/CLI/block/snapshot/schedule_list.py +++ b/SoftLayer/CLI/block/snapshot/schedule_list.py @@ -7,11 +7,11 @@ from SoftLayer.CLI import formatting -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('volume_id') @environment.pass_env def cli(env, volume_id): - """Lists snapshot schedules for a given volume""" + """Lists snapshot schedules for a given volume.""" block_manager = SoftLayer.BlockStorageManager(env.client) diff --git a/SoftLayer/CLI/block/snapshot/set_notify_status.py b/SoftLayer/CLI/block/snapshot/set_notify_status.py new file mode 100644 index 000000000..053a959ee --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/set_notify_status.py @@ -0,0 +1,27 @@ +"""Disable/Enable snapshots space usage threshold warning for a specific volume""" +# :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('volume_id') +@click.option( + '--enable/--disable', + default=True, + help=""" + Enable/Disable snapshot notification. Use `slcli block snapshot-set-notification volumeId --enable` to enable. + """, + required=True) +@environment.pass_env +def cli(env, volume_id, enable): + """Enables/Disables snapshot space usage threshold warning for a given volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + + block_manager.set_volume_snapshot_notification(volume_id, enable) + + click.echo( + 'Snapshots space usage threshold warning notification has bee set to %s for volume %s' + % (enable, volume_id)) diff --git a/SoftLayer/CLI/block/subnets/assign.py b/SoftLayer/CLI/block/subnets/assign.py index 3ff2ab758..d80e5aff4 100644 --- a/SoftLayer/CLI/block/subnets/assign.py +++ b/SoftLayer/CLI/block/subnets/assign.py @@ -6,7 +6,7 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('access_id', type=int) @click.option('--subnet-id', multiple=True, type=int, help="ID of the subnets to assign; e.g.: --subnet-id 1234") @@ -14,6 +14,12 @@ def cli(env, access_id, subnet_id): """Assign block storage subnets to the given host id. + EXAMPLE:: + + slcli block subnets-assign 111111 --subnet-id 222222 + slcli block subnets-assign 111111 --subnet-id 222222 --subnet-id 333333 + ACCESS_ID is the host_id obtained by: softlayer slcli block access-list + access_id is the host_id obtained by: slcli block access-list SoftLayer_Account::iscsiisolationdisabled must be False to use this command @@ -24,14 +30,14 @@ def cli(env, access_id, subnet_id): assigned_subnets = block_manager.assign_subnets_to_acl(access_id, subnet_ids) for subnet in assigned_subnets: - message = "Successfully assigned subnet id: {} to allowed host id: {}".format(subnet, access_id) + message = f"Successfully assigned subnet id: {subnet} to allowed host id: {access_id}" click.echo(message) failed_to_assign_subnets = list(set(subnet_ids) - set(assigned_subnets)) for subnet in failed_to_assign_subnets: - message = "Failed to assign subnet id: {} to allowed host id: {}".format(subnet, access_id) + message = f"Failed to assign subnet id: {subnet} to allowed host id: {access_id}" click.echo(message) except SoftLayer.SoftLayerAPIError as ex: - message = "Unable to assign subnets.\nReason: {}".format(ex.faultString) + message = f"Unable to assign subnets.\nReason: {ex.faultString}" click.echo(message) diff --git a/SoftLayer/CLI/block/subnets/list.py b/SoftLayer/CLI/block/subnets/list.py index d7576971a..297846abd 100644 --- a/SoftLayer/CLI/block/subnets/list.py +++ b/SoftLayer/CLI/block/subnets/list.py @@ -10,18 +10,22 @@ COLUMNS = [ 'id', - 'createDate', 'networkIdentifier', 'cidr' ] -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('access_id', type=int) @environment.pass_env def cli(env, access_id): """List block storage assigned subnets for the given host id. + Example:: + + slcli block subnets-list 12345678 + ACCESS_ID is the host_id obtained by: softlayer slcli block access-list + access_id is the host_id obtained by: slcli block access-list """ @@ -32,10 +36,9 @@ def cli(env, access_id): table = formatting.Table(COLUMNS) for subnet in subnets: - row = ["{0}".format(subnet['id']), - "{0}".format(subnet['createDate']), - "{0}".format(subnet['networkIdentifier']), - "{0}".format(subnet['cidr'])] + row = [f"{subnet['id']}", + f"{subnet['networkIdentifier']}", + f"{subnet['cidr']}"] table.add_row(row) env.fout(table) diff --git a/SoftLayer/CLI/block/subnets/remove.py b/SoftLayer/CLI/block/subnets/remove.py index d700fb1dd..98d52ad11 100644 --- a/SoftLayer/CLI/block/subnets/remove.py +++ b/SoftLayer/CLI/block/subnets/remove.py @@ -6,7 +6,7 @@ from SoftLayer.CLI import environment -@click.command() +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) @click.argument('access_id', type=int) @click.option('--subnet-id', multiple=True, type=int, help="ID of the subnets to remove; e.g.: --subnet-id 1234") @@ -14,6 +14,12 @@ def cli(env, access_id, subnet_id): """Remove block storage subnets for the given host id. + Example:: + + slcli block subnets-remove 111111 --subnet-id 222222 + slcli block subnets-remove 111111 --subnet-id 222222 --subnet-id 333333 + ACCESS_ID is the host_id obtained by: slcli block access-list + access_id is the host_id obtained by: slcli block access-list SoftLayer_Account::iscsiisolationdisabled must be False to use this command @@ -24,14 +30,14 @@ def cli(env, access_id, subnet_id): removed_subnets = block_manager.remove_subnets_from_acl(access_id, subnet_ids) for subnet in removed_subnets: - message = "Successfully removed subnet id: {} for allowed host id: {}".format(subnet, access_id) + message = f"Successfully removed subnet id: {subnet} for allowed host id: {access_id}" click.echo(message) failed_to_remove_subnets = list(set(subnet_ids) - set(removed_subnets)) for subnet in failed_to_remove_subnets: - message = "Failed to remove subnet id: {} for allowed host id: {}".format(subnet, access_id) + message = f"Failed to remove subnet id: {subnet} for allowed host id: {access_id}" click.echo(message) except SoftLayer.SoftLayerAPIError as ex: - message = "Unable to remove subnets.\nReason: {}".format(ex.faultString) + message = f"Unable to remove subnets.\nReason: {ex.faultString}" click.echo(message) diff --git a/SoftLayer/CLI/call_api.py b/SoftLayer/CLI/call_api.py index b07834ed6..e6c007e25 100644 --- a/SoftLayer/CLI/call_api.py +++ b/SoftLayer/CLI/call_api.py @@ -3,6 +3,7 @@ import click +from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting @@ -76,10 +77,9 @@ def _validate_filter(ctx, param, value): # pylint: disable=unused-argument try: _filter = json.loads(value) if not isinstance(_filter, dict): - raise exceptions.CLIAbort("\"{}\" should be a JSON object, but is a {} instead.". - format(_filter, type(_filter))) + raise exceptions.CLIAbort(f"\"{_filter}\" should be a JSON object, but is a {type(_filter)} instead.") except json.JSONDecodeError as error: - raise exceptions.CLIAbort("\"{}\" is not valid JSON. {}".format(value, error)) + raise exceptions.CLIAbort(f"\"{value}\" is not valid JSON. {error}") return _filter @@ -95,13 +95,13 @@ def _validate_parameters(ctx, param, value): # pylint: disable=unused-argument try: parameter = json.loads(parameter) except json.JSONDecodeError as error: - click.secho("{} looked like json, but was invalid, passing to API as is. {}". - format(parameter, error), fg='red') + click.secho(f"{parameter} looked like json, but was invalid, passing to API as is. {error}", + fg='red') validated_values.append(parameter) return validated_values -@click.command('call', short_help="Call arbitrary API endpoints.") +@click.command('call', short_help="Call arbitrary API endpoints.", cls=SLCommand) @click.argument('service') @click.argument('method') @click.argument('parameters', nargs=-1, callback=_validate_parameters) @@ -149,6 +149,9 @@ def cli(env, service, method, parameters, _id, _filters, mask, limit, offset, or '[{"keyName": "NETWORK_MESSAGE_DELIVERY_MANAGE"}]' slcli call-api Account getVirtualGuests \\ --orderBy virttualguests.id=ASC + slcli call-api SoftLayer_Notification_Occurrence_Event getAllObjects \\ + --json-filter='{"endDate": {"operation": "greaterThanDate", \\ + "options": [{"name":"date", "value": ["10/14/2022"]}]}}' --limit=50 """ if _filters and json_filter: @@ -171,7 +174,7 @@ def cli(env, service, method, parameters, _id, _filters, mask, limit, offset, or } if output_python: - env.out(_build_python_example(args, kwargs)) + env.python_output(_build_python_example(args, kwargs)) else: result = env.client.call(*args, **kwargs) env.fout(formatting.iter_to_table(result)) 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/detail.py b/SoftLayer/CLI/cdn/detail.py deleted file mode 100644 index ef93d2794..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() -@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_radio', hit_ratio]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/edit.py b/SoftLayer/CLI/cdn/edit.py deleted file mode 100644 index f39e20a02..000000000 --- a/SoftLayer/CLI/cdn/edit.py +++ /dev/null @@ -1,81 +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() -@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('--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', multiple=True, 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 a description too using again --cache, " - "e.g --cache=include-specified --cache=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, origin, respect_headers, cache, 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: - if len(cache) > 1: - cache_result['cacheKeyQueryRule'] = cache[0] - cache_result['description'] = cache[1] - else: - cache_result['cacheKeyQueryRule'] = cache[0] - - cdn_result = manager.edit(cdn_id, header=header, http_port=http_port, origin=origin, - respect_headers=respect_headers, cache=cache_result, - 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')]) - table.add_row(['Http Port', cdn.get('httpPort')]) - 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 994a338b3..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() -@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 08790d9b7..000000000 --- a/SoftLayer/CLI/cdn/origin_add.py +++ /dev/null @@ -1,86 +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() -@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('--port', '-p', - type=click.INT, - help="The http port number.", - default=80, - show_default=True) -@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']), - help="Performance configuration", - default='web', - 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, port, protocol, optimize_for, 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, origin_type=origin_type, - header=header, port=port, protocol=protocol, - bucket_name=bucket_name, file_extensions=extensions, - optimize_for=optimize_for, 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(['Path', result['path']]) - table.add_row(['Port', result['httpPort']]) - 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 208c26f61..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() -@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 4e4172387..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() -@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 26bff1dd2..000000000 --- a/SoftLayer/CLI/cdn/purge.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Purge cached files from all edge nodes.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command() -@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: - table.add_row([ - data['date'], - data['path'], - data['saved'], - data['status'] - ]) - - env.fout(table) diff --git a/SoftLayer/CLI/command.py b/SoftLayer/CLI/command.py new file mode 100644 index 000000000..34c50e549 --- /dev/null +++ b/SoftLayer/CLI/command.py @@ -0,0 +1,259 @@ +""" + SoftLayer.CLI.command + ~~~~~~~~~~~~~~~~~~~~~ + Command interface for the SoftLayer CLI. Basically the Click commands, with fancy help text + + :license: MIT, see LICENSE for more details. +""" +import inspect +import types + +import click + +from rich import box +from rich.highlighter import RegexHighlighter +from rich.table import Table +from rich.text import Text + +from SoftLayer.CLI import environment + + +class OptionHighlighter(RegexHighlighter): + """Provides highlighter regex for the Command help. + + Defined in SoftLayer\\utils.py console_color_themes() + """ + highlights = [ + r"(?P^\-\w)", # single options like -v + r"(?P