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/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..5d10555b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: Bug +assignees: '' + +--- + +> Reminder: No username or APIkeys should be added to these issues, as they are public. + + +**Describe the bug** +A clear and concise description of what the bug is. Include the command you used, make sure to include the `-v` flag, as that information is very helpful. Ex: `slcli -v vs list` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Version** +Include the output of `slcli --version` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..b8a79ec6f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: New Feature +assignees: '' + +--- + +> REMINDER: Never add usernames or apikeys in these issues, as they are public. + +**What are you trying to do?** +A brief explanation of what you are trying to do. Could be something simple like `slcli vs list` doesn't support a filter you need. Or more complex like recreating some functionality that exists in the cloud.ibm.com portal + +**Screen shots** +If the functionality you want exists in the portal, please add a screenshot so we have a better idea of what you need. + +**Additional context** +Add any other context or screenshots about the feature request here. 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 new file mode 100644 index 000000000..d43890b44 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '41 6 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + 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. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # 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@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 000000000..f09354735 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,27 @@ +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 new file mode 100644 index 000000000..95afeced1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +# 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: + 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@v4 + - 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: 📦 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 new file mode 100644 index 000000000..70245307b --- /dev/null +++ b/.github/workflows/test_pypi_release.yml @@ -0,0 +1,42 @@ +# 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 new file mode 100644 index 000000000..35ae72725 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,70 @@ +name: Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8,3.9,'3.10',3.11,3.12] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tools/test-requirements.txt + - name: Tox Test + run: tox -e py + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tools/test-requirements.txt + - name: Tox Coverage + run: tox -e coverage + analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + 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/.travis.yml b/.travis.yml deleted file mode 100644 index d3cc13a76..000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -# https://docs.travis-ci.com/user/languages/python/#python-37-and-higher -dist: xenial -language: python -sudo: false -matrix: - include: - - python: "3.5" - env: TOX_ENV=py35 - - python: "3.6" - env: TOX_ENV=py36 - - python: "3.7" - env: TOX_ENV=py37 - - python: "pypy3.5" - env: TOX_ENV=pypy3 - - python: "3.6" - env: TOX_ENV=analysis - - python: "3.6" - env: TOX_ENV=coverage -install: - - pip install tox - - pip install coveralls -script: - - tox -e $TOX_ENV -after_success: - - if [[ $TOX_ENV = "coverage" ]]; then coveralls; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff0afc50..abee1bea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,461 @@ # 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.8.5] - 2019-12-20 +## [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 + +#### Improvements +- Fixed some doc block issues when generating HTML #1513 +- Updates to the Release workflow for publishing to test pypi #1514 + +- Adding in CodeQL Analysis #1517 +- Create SECURITY.md #1518 +- Fix the network space is empty on subnet detail #1523 +- Prevents SLCLI_VERSION environment variable from breaking things #1527 +- Refactor loadbal order-options #1521 +- slcli server create-options dal13 Error #1526 + +#### New Commands +- add new feature on vlan cli #1499 + + `slcli vlan create` + +## [5.9.6] - 2021-07-05 +https://github.com/softlayer/softlayer-python/compare/v5.9.5...v5.9.6 + +#### Improvements +- Updated snap to core20 and edited README #1494 +- Add a table result for `slcli hw upgrade` output. #1488 +- Remove block/file interval option for replica volume. #1497 +- `slcli vlan cancel` should report if a vlan is automatic. #1495 +- New method to manage how long text is in output tables. #1506 +- Fix Tox-analysis issues. #1510 + +#### New Commands +- add new email feature #1483 + + `slcli email list` + + `slcli email detail` + + `slcli email edit` +- `slcli vlan cancel` +- Add slcli account licenses #1501 + + `slcli account licenses` +- Create a new commands on slcli that create/cancel a VMware licenses #1504 + + `slcli licenses create` + + `slcli licenses cancel` + + +## [5.9.5] - 2021-05-25 +https://github.com/softlayer/softlayer-python/compare/v5.9.4...v5.9.5 + +#### Improvements +- Changed a testing domain to one that really doesnt exist #1492 +- Fix Incomplete notes field for file and block #1484 +- Show component versions on hw detail #1470 +- Add the firewall information on slcli firewall detail #1475 +- Add an --orderBy parameters to call-api #1459 +- Add image detail transaction data #1479 + + + +## [5.9.4] - 2021-04-27 +https://github.com/softlayer/softlayer-python/compare/v5.9.3...v5.9.4 + +#### New Commands +- `slcli hw authorize-storage` #1439 +- `slcli order quote-save` #1451 + + +#### Improvements + +- Refactored managers.ordering_manager.verify_quote() to work better with the REST endpoing #1430 +- Add routers for each DC in slcli hw create-options #1432 +- Add preset datatype in slcli virtual detail #1435 +- Add upgrade option to slcli hw. #1437 +- Ibmcloud authentication support #1315 / #1447 + + `slcli config setup --ibmid` + + `slcli config setup --sso` + + `slcli config setup --cloud_key` + + `slcli config setup --classic_key` +- Refactor slcli hw detail prices. #1443 +- Updated contributing guide #1458 +- Add the Hardware components on "slcli hardware detail" #1452 +- Add billing and lastTransaction on hardware detail #1446 +- Forced reserved capacity guests to be monthly #1454 +- Removing the rwhois commands #1456 +- Added automation to publish to test-pypi #1467 +- Updating author_email to SLDN distro list #1469 +- Add the option to add and upgrade the hw disk. #1455 +- Added a utility to merge objectFilters, #1468 +- Fixes shift+ins when pasteing into a password field for windows users. #1460 +- Add Billing and lastTransaction on slcli virtual detail #1466 +- Fixing 'import mock' pylint issues #1476 + + +## [5.9.3] - 2021-03-03 +https://github.com/softlayer/softlayer-python/compare/v5.9.2...v5.9.3 + +#### New Commands +- `slcli file|block disaster-recovery-failover` #1407 + +#### Improvements +- Unit testing for large integers #1403 +- Add Multi factor authentication to users list #1408 +- Add pagination to object storage list accounts. #1411 +- Add username lookup to slcli object-storage credential #1415 +- Add IOPs data to slcli block volume-list. #1418 +- Add 2FA and classic APIKeys fields to slcli user list as default values #1421 +- Add a flags in the report bandwidth #1420 +- Add the option network component by router to slcli hw create. #1422 +- Add slcli vs create by router data. #1414 +- Add testing and support for python 3.9. #1429 +- Checking for TermLength on prices #1428 + + + +## [5.9.2] - 2020-12-03 +https://github.com/softlayer/softlayer-python/compare/v5.9.1...v5.9.2 + +#### New Commands +- `slcli account orders` #1349 +- `slcli order lookup` #1354 + +#### Improvements +- Ordering price information improvements. #1319 +- refactor vsi create-option #1337 +- Add Invoice Item id as parameter in `slcli account item-detail` command +- Added order lookup command to block and file orders. #1350 +- Add prices to vs create-options. #1351 +- Allow orders without a location if needed #1356 +- Refactor file and block commands to use the username resolver #1357 +- Fix create subnet static for ipv4 price. #1358 +- moved snapcraft readme #1363 +- Update snapcraft.yaml #1365 +- Updated documentation on how to deal with KeyError #1366 +- Fix order item-list --prices location #1360 +- Removed Nessus scanner from docs and examples #1368 +- Fix subnet list. #1379 +- Fixed analysis/flake8 tests #1381 +- Remove the `-a` option from `slcli user create`. Only the user themselves can create an API key now. #1377 + +## [5.9.1] - 2020-09-15 +https://github.com/softlayer/softlayer-python/compare/v5.9.0...v5.9.1 + +- Fix the ha option for firewalls, add and implement unit test #1327 +- BluePages_Search and IntegratedOfferingTeam_Region don't need SoftLayer_ prefix #972 +- Fix new TOX issues #1330 +- Add more unit test coverage #1331 +- Set notes for network storage #1322 +- Some improvements to the dns commands #999 + + dns zone-list: added resourceRecordCount, added automatic pagination for large zones + + dns record-list: fixed an issue where a record (like SRV types) that don't have a host would cause the command to fail +- Renamed managers.storage.refresh_dep_dupe to SoftLayer.managers.storage.refresh_dupe #1342 to support the new API method. CLI commands now use this method. +- #1295 added disk upgrade options for virtual guests + +## [5.9.0] - 2020-08-03 +https://github.com/softlayer/softlayer-python/compare/v5.8.9...v5.9.0 + +- #1280 Notification Management + + slcli user notifications + + slcli user edit-notifications +- #828 Added networking options to slcli hw create-options + + Refactored slcli hw create to use the ordering manager + + Added --network option to slcli hw create for more granular network choices. + + Deprecated --port-speed and --no-public . They still work for now, but will be removed in a future release. +- #1298 Fix Unhandled exception in CLI - vs detail +- #1309 Fix the empty lines in slcli vs create-options +- #1301 Ability to list VirtualHost capable guests + + slcli hardware guests + + slcli vs list will show guests on VirtualHost servers +- #875 added option to reload bare metal servers with LVM enabled +- #874 Added Migrate command +- #1313 Added support for filteredMask +- #1305 Update docs links +- #1302 Fix lots of whitespace slcli vs create-options +- #900 Support for STDIN on creating and updating tickets. +- #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 +https://github.com/softlayer/softlayer-python/compare/v5.8.8...v5.8.9 + +- #1252 Automated Snap publisher +- #1230 Tag Management + + slcli tags cleanup + + slcli tags delete + + slcli tags details + + slcli tags list + + slcli tags set + + slcli tags taggable +- #1285 Vlan editing functionality +- #1287 Edit IP note and add ipAddress table in detail view +- #1283 Subnet Tagging +- #1291 Storage documentation updates +- #1293 add system operation referenceCode in create-option + +## [5.8.8] - 2020-05-18 +https://github.com/softlayer/softlayer-python/compare/v5.8.7...v5.8.8 + +- #1266 Fixed ticket upload with REST endpoint +- #1263 add the redundant/degraded option to hardware +- #1262 Added `iter` option for ordering manager functions +- #1264 Add Account planned, unplanned and announcement events +- #1265 fixed pylint 2.5.0 errors +- #1261 Fix AttributeError: 'NoneType' object has no attribute 'keys +- #1256 Adding more github action tests, removing travis CI tests +- #887 fix Response shows additional new lines (\n) in ticket details +- #1241 Storage feature for virtual and hardware servers +- #1242 Hardware and Virtual billing info +- #1239 VPN subnet access to a use +- #1254 added account billing-items/item-details/cancel-item commands + +## [5.8.7] - 2020-03-26 +https://github.com/softlayer/softlayer-python/compare/v5.8.5...v5.8.7 + +- #1222 Get load balancer (LBaaS) by name +- #1221 Added version checker +- #1227 Updated unit test suite for TravisCI to run properly +- #1225 Add note about using multiple colon symbols not working when setting tags. +- #1228 Support ordering [Dependent Duplicate Volumes](https://cloud.ibm.com/docs/BlockStorage?topic=BlockStorage-dependentduplicate) +- #1233 Refactored File/Block managers to reduce duplicated code. +- #1231 Added Refresh functions for Dependent Duplicate Volumes +- #801 Added support for JSON styled parameters and object filters +- #1234 Added ability to change which datacenters an image template was stored in + +## [5.8.6] - Skipped + +## [5.8.5] - 2020-01-29 +https://github.com/softlayer/softlayer-python/compare/v5.8.4...v5.8.5 + +- #1195 Fixed an issue with `slcli vs dns-sync --ptr`. Added `slcli hw dns-sync` +- #1199 Fix File Storage failback and failover. +- #1198 Fix issue where the summary command fails due to None being provided as the datacenter name. +- #1208 Added The following commands: + - `slcli block volume-limits` + - `slcli file volume-limits` +- #1209 Add testing/CI for python 3.8. +- #1212 Fix vs detail erroring on servers pending cancellation. +- #1210 support subnet ACL management through cli + + `slcli block subnets-list` + + `slcli block subnets-assign` + + `slcli block subnets-remove` +- #1215 Added documentation for all SLCLI commands. + + +## [5.8.4] - 2019-12-20 https://github.com/softlayer/softlayer-python/compare/v5.8.3...v5.8.4 - #1199 Fix block storage failback and failover. @@ -162,7 +616,7 @@ https://github.com/softlayer/softlayer-python/compare/v5.8.2...v5.8.3 ## [5.6.0] - 2018-10-16 - Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.3...v5.6.0 -+ #1026 Support for [Reserved Capacity](https://console.bluemix.net/docs/vsi/vsi_about_reserved.html#about-reserved-virtual-servers) ++ #1026 Support for [Reserved Capacity](https://cloud.ibm.com/docs/virtual-servers?topic=virtual-servers-about-reserved-virtual-servers) * `slcli vs capacity create` * `slcli vs capacity create-guest` * `slcli vs capacity create-options` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1eed6d308..45bce3aff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,14 +3,22 @@ We are happy to accept contributions to softlayer-python. Please follow the guidelines below. -* Sign our contributor agreement (CLA) You can find the [CLA here](./docs/dev/cla-individual.md). +## Procedural -* If you're contributing on behalf of your employer we'll need a signed copy of our corporate contributor agreement (CCLA) as well. You can find the [CCLA here](./docs/dev/cla-corporate.md). - -* Fork the repo, make your changes, and open a pull request. +1. All code changes require a corresponding issue. [Open an issue here](https://github.com/softlayer/softlayer-python/issues). +2. Fork the [softlayer-python](https://github.com/softlayer/softlayer-python) repository. +3. Make any changes required, commit messages should reference the issue number (include #1234 if the message if your issue is number 1234 for example). +4. Make a pull request from your fork/branch to origin/master +5. Requires 1 approval for merging * Additional infomration can be found in our [contribution guide](http://softlayer-python.readthedocs.org/en/latest/dev/index.html) +## Legal + +* See our [Contributor License Agreement](./docs/dev/cla-individual.md). Opening a pull request is acceptance of this agreement. + +* If you're contributing on behalf of your employer we'll need a signed copy of our corporate contributor agreement (CCLA) as well. You can find the [CCLA here](./docs/dev/cla-corporate.md). + ## Code style @@ -32,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 @@ -101,4 +130,72 @@ order_args = getattr(order_call[0], 'args')[0] # Test our specific argument value self.assertEqual(123, order_args['hostId']) +``` + + +## Project Management + +### Issues + +* _Title_: Should contain quick highlight of the issue is about +* _Body_: All the technical information goes here +* _Assignee_: Should be the person who is actively working on an issue. +* _Label_: All issues should have at least 1 Label. +* _Projects_: Should be added to the quarerly Softlayer project when being worked on +* _Milestones_: Not really used, can be left blank +* _Linked Pull Request_: Should be linked to the relavent pull request when it is opened. + +### Pull Requests + +* _Title_: Should be similar to the title of the issue it is fixing, or otherwise descibe what is chaning in the pull request +* _Body_: Should have "Fixes #1234" at least, with some notes about the specific pull request if needed. Most technical information should still be in the github issue. +* _Reviewers_: 1 Reviewer is required +* _Assignee_: Should be the person who opened the pull request +* _Labels_: Should match the issue +* _Projects_: Should match the issue +* _Milestones_: Not really used, can be left blank +* _Linked issues_: If you put "Fixes #" in the body, this should be automatically filled in, otherwise link manually. + +### Code Reviews +All issues should be reviewed by at least 1 member of the SLDN team that is not the person opening the pull request. Time permitting, all members of the SLDN team should review the request. + +#### Things to look for while doing a review + +As a reviewer, these are some guidelines when doing a review, but not hard rules. + +* Code Style: Generally `tox -e analysis` will pick up most style violations, but anything that is wildly different from the normal code patters in this project should be changed to match, unless there is a strong reason to not do so. +* API Calls: Close attention should be made to any new API calls, to make sure they will work as expected, and errors are handled if needed. +* DocBlock comments: CLI and manager methods need to be documented well enough for users to easily understand what they do. +* Easy to read code: Code should generally be easy enough to understand at a glance. Confusing code is a sign that it should either be better documented, or refactored a bit to be clearer in design. + + +### Testing + +When doing testing of a code change, indicate this with a comment on the pull request like + +: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/CONTRIBUTORS b/CONTRIBUTORS index 4de814b58..5b475dc73 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,6 +6,7 @@ chechuironman Christopher Gallo David Ibarra Hans Kristian Moen +Ian Sutton Jake Williams Jason Johnson Kevin Landreth 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/snap/README.md b/README-snapcraft.md similarity index 100% rename from snap/README.md rename to README-snapcraft.md diff --git a/README.rst b/README.rst index cdc376753..29536d085 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,17 @@ SoftLayer API Python Client =========================== -.. image:: https://travis-ci.org/softlayer/softlayer-python.svg?branch=master - :target: https://travis-ci.org/softlayer/softlayer-python - -.. image:: https://landscape.io/github/softlayer/softlayer-python/master/landscape.svg - :target: https://landscape.io/github/softlayer/softlayer-python/master - +.. 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://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://build.snapcraft.io/badge/softlayer/softlayer-python.svg - :target: https://build.snapcraft.io/user/softlayer/softlayer-python - +.. 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 `_. @@ -25,8 +22,7 @@ SoftLayer products and services. Documentation ------------- -Documentation for the Python client is available at -http://softlayer.github.io/softlayer-python/. +Documentation for the Python client is available at `Read the Docs `_ . Additional API documentation can be found on the SoftLayer Development Network: @@ -35,7 +31,7 @@ Additional API documentation can be found on the SoftLayer Development Network: * `Object mask information and examples `_ * `Code Examples - `_ + `_ Installation ------------ @@ -58,7 +54,12 @@ To install the slcli snap: .. code-block:: bash - $ sudo snap install slcli + $ sudo snap install slcli + + (or to get the latest release) + + $ sudo snap install slcli --edge + @@ -69,6 +70,29 @@ InsecurePlatformWarning Notice ------------------------------ This library relies on the `requests `_ library to make HTTP requests. On Python versions below Python 2.7.9, requests has started emitting a security warning (InsecurePlatformWarning) due to insecurities with creating SSL connections. To resolve this, upgrade to Python 2.7.9+ or follow the instructions here: http://stackoverflow.com/a/29099439. +Basic Usage +----------- + +- `The Complete Command Directory `_ + +Advanced Usage +-------------- + +You can automatically set some parameters via environment variables with by using the SLCLI prefix. For example + +.. code-block:: bash + + $ export SLCLI_VERBOSE=3 + $ export SLCLI_FORMAT=json + $ slcli vs list + +is equivalent to + +.. code-block:: bash + + $ slcli -vvv --format=json vs list + + Getting Help ------------ Bugs and feature requests about this library should have a `GitHub issue `_ opened about them. @@ -79,7 +103,13 @@ Issues with the Softlayer API itself should be addressed by opening a ticket. Examples -------- -A curated list of examples on how to use this library can be found at `softlayer.github.io `_ +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 --------- @@ -124,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, or 3.7. +* 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 `_ . @@ -138,15 +173,17 @@ 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 --------- -This software is Copyright (c) 2016-2019 SoftLayer Technologies, Inc. +This software is Copyright (c) 2016-2021 SoftLayer Technologies, Inc. See the bundled LICENSE file for more information. diff --git a/RELEASE.md b/RELEASE.md index eb1cb6d47..962ee1663 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,18 +1,74 @@ -# Release steps -* Update version constants (find them by running `git grep [VERSION_NUMBER]`) -* Create changelog entry (edit CHANGELOG.md with a one-liner for each closed issue going in the release) -* Commit and push changes to master with the message: "Version Bump to v[VERSION_NUMBER]" -* Make sure your `upstream` repo is set + +# Versions + +This project follows the Major.Minor.Revision versioning system. Fixes, and minor additions would increment Revision. Large changes and additions would increment Minor, and anything that would be a "Breaking" change, or redesign would be an increment of Major. + +# Changelog + +When doing a release, the Changelog format should be as follows: + +```markdown + +## [Version] - YYYY-MM-DD +https://github.com/softlayer/softlayer-python/compare/v5.9.0...v5.9.1 + +#### New Command +- `slcli new command` #issueNumber + +#### Improvements +- List out improvements #issueNumber +- Something else that changed #issueNumber + +#### Deprecated +- List something that got removed #issueNumber + +``` + +# Normal Release steps + +A "release" of the softlayer-python project is the current state of the `master` branch. Any changes in the master branch should be considered releaseable. + + +1. Create the changelog entry, us this to update `CHANGELOG.md` and as the text for the release on github. +2. Update the version numbers in these files on the master branch. + - `SoftLayer/consts.py` + - `setup.py` +3. Make sure the tests for the build all pass +4. [Draft a new release](https://github.com/softlayer/softlayer-python/releases/new) + - Version should start with `v` followed by Major.Minor.Revision: `vM.m.r` + - Title should be `M.m.r` + - Description should be the release notes + - Target should be the `master` branch +5. The github automation should take care of publishing the release to [PyPi](https://pypi.org/project/SoftLayer/). This may take a few minutes to update. + +# Manual Release steps + +1. Create the changelog entry, us this to update `CHANGELOG.md` and as the text for the release on github. +2. Update the version numbers in these files on the master branch. + - `SoftLayer/consts.py` + - `setup.py` +3. Commit your changes to `master`, and make sure `softlayer/softlayer-python` repo is updated to reflect that +4. Make sure your `upstream` repo is set + ``` git remote -v upstream git@github.com:softlayer/softlayer-python.git (fetch) upstream git@github.com:softlayer/softlayer-python.git (push) ``` -* Push tag and PyPi `python fabfile.py 5.7.2`. Before you do this, make sure you have the organization repository set up as upstream remote, also make sure that you have pip set up with your PyPi user credentials. The easiest way to do that is to create a file at `~/.pypirc` with the following contents: + +5. Create and publish the package + - Make sure you have `twine` installed, this is what uploads the pacakge to PyPi. + - Before you do this, make sure you have the organization repository set up as upstream remote, also make sure that you have pip set up with your PyPi user credentials. The easiest way to do that is to create a file at `~/.pypirc` with the following contents: ``` [server-login] username:YOUR_USERNAME password:YOUR_PASSWORD ``` + + - Run `python fabfile.py 5.7.2`. Where `5.7.2` is the `M.m.r` version number. Don't use the `v` here in the version number. + + +*NOTE* PyPi doesn't let you reupload a version, if you upload a bad package for some reason, you have to create a new version. + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..72ec7632d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +Generally only the latest release will be actively worked on and supported. +Version 5.7.2 is the last version that supports python2.7. + +| Version | Supported | +| ------- | ------------------ | +| 6.2.x | :white_check_mark: | +| 5.7.2 | :white_check_mark: | +| < 5.7.2 | :x: | + +## Reporting a Vulnerability + +Create a new [Bug Report](https://github.com/softlayer/softlayer-python/issues/new?assignees=&labels=Bug&template=bug_report.md&title=) to let us know about any vulnerabilities in the code base. diff --git a/SoftLayer/API.py b/SoftLayer/API.py index b20b13aaa..cff277286 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -6,22 +6,35 @@ :license: MIT, see LICENSE for more details. """ # pylint: disable=invalid-name -import warnings +import time +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 API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT +CONFIG_FILE = consts.CONFIG_FILE + __all__ = [ 'create_client_from_env', + 'employee_client', 'Client', 'BaseClient', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', + 'IAMClient', + 'CertificateClient' ] VALID_CALL_ARGS = set(( @@ -33,7 +46,7 @@ 'raw_headers', 'limit', 'offset', - 'verify', + 'verify' )) @@ -80,6 +93,8 @@ def create_client_from_env(username=None, 'Your Company' """ + if config_file is None: + config_file = CONFIG_FILE settings = config.get_client_settings(username=username, api_key=api_key, endpoint_url=endpoint_url, @@ -127,16 +142,91 @@ def create_client_from_env(username=None, settings.get('api_key'), ) - return BaseClient(auth=auth, transport=transport) + return BaseClient(auth=auth, transport=transport, config_file=config_file) -def Client(**kwargs): - """Get a SoftLayer API Client using environmental settings. +def employee_client(username=None, + access_token=None, + endpoint_url=None, + timeout=None, + auth=None, + config_file=None, + proxy=None, + user_agent=None, + transport=None, + verify=True): + """Creates an INTERNAL SoftLayer API client using your environment. + + Settings are loaded via keyword arguments, environemtal variables and config file. - Deprecated in favor of create_client_from_env() + :param username: your user ID + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token) + :param password: password to use for employee authentication + :param endpoint_url: the API endpoint base URL you wish to connect to. + Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. + :param proxy: proxy to be used to make API calls + :param integer timeout: timeout for API requests + :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. + Example: `BasicAuthentication` + :param config_file: A path to a configuration file used to load settings + :param user_agent: an optional User Agent to report when making API + calls if you wish to bypass the packages built in User Agent string + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) + :param bool verify: decide to verify the server's SSL/TLS cert. """ - warnings.warn("use SoftLayer.create_client_from_env() instead", - DeprecationWarning) + settings = config.get_client_settings(username=username, + api_key=None, + endpoint_url=endpoint_url, + timeout=timeout, + proxy=proxy, + verify=None, + config_file=config_file) + + url = settings.get('endpoint_url', '') + verify = settings.get('verify', True) + + if 'internal' not in url: + raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.") + + if transport is None: + if url is not None and '/rest' in url: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + else: + # Default the transport to use XMLRPC + transport = transports.XmlRpcTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + + if access_token is None: + access_token = settings.get('access_token') + + user_id = settings.get('userid') + # Assume access_token is valid for now, user has logged in before at least. + if settings.get('auth_cert', False): + auth = slauth.X509Authentication(settings.get('auth_cert'), verify) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + elif access_token and user_id: + auth = slauth.EmployeeAuthentication(user_id, access_token) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + else: + # This is for logging in mostly. + LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.") + return EmployeeClient(auth=None, transport=transport, config_file=config_file) + + +def Client(**kwargs): + """Get a SoftLayer API Client using environmental settings.""" return create_client_from_env(**kwargs) @@ -144,26 +234,61 @@ 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_" - - def __init__(self, auth=None, transport=None): + auth: slauth.AuthenticationBase + + def __init__(self, auth=None, transport=None, config_file=None): + if config_file is None: + config_file = CONFIG_FILE + 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: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + 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', 0)), + user_agent=consts.USER_AGENT, + 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', 0)), + user_agent=consts.USER_AGENT, + 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 :param string password: your SoftLayer password :param int security_question_id: The security question id to answer - :param string security_question_answer: The answer to the security - question + :param string security_question_answer: The answer to the security question """ self.auth = None @@ -202,8 +327,7 @@ def call(self, service, method, *args, **kwargs): :param dict raw_headers: (optional) HTTP transport headers :param int limit: (optional) return at most this many results :param int offset: (optional) offset results by this many - :param boolean iter: (optional) if True, returns a generator with the - results + :param boolean iter: (optional) if True, returns a generator with the results :param bool verify: verify SSL cert :param cert: client certificate path @@ -221,10 +345,10 @@ 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)) - if self._prefix and not service.startswith(self._prefix): + prefixes = (self._prefix, 'BluePages_Search', 'IntegratedOfferingTeam_Region') + if self._prefix and not service.startswith(prefixes): service = self._prefix + service http_headers = {'Accept': '*/*'} @@ -247,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', {})) @@ -286,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 @@ -314,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) @@ -323,6 +477,303 @@ 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 + + :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 authenticate_with_password(self, username, password, security_question_id=None, security_question_answer=None): + """Performs IBM IAM Username/Password Authentication + + :param string username: your IBMid username + :param string password: your IBMid password + """ + + iam_client = requests.Session() + + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': consts.USER_AGENT, + 'Accept': 'application/json' + } + data = { + 'grant_type': 'password', + 'password': password, + 'response_type': 'cloud_iam', + 'username': username + } + + try: + response = iam_client.request( + 'POST', + 'https://iam.cloud.ibm.com/identity/token', + data=data, + headers=headers, + auth=requests.auth.HTTPBasicAuth('bx', 'bx') + ) + if response.status_code != 200: + LOGGER.error("Unable to login: %s", response.text) + + response.raise_for_status() + tokens = json.loads(response.text) + except requests.HTTPError as ex: + error = json.loads(response.text) + raise exceptions.IAMError(response.status_code, + error.get('errorMessage'), + 'https://iam.cloud.ibm.com/identity/token') from ex + + self.settings['softlayer']['access_token'] = tokens['access_token'] + self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] + + config.write_config(self.settings, self.config_file) + self.auth = slauth.BearerAuthentication('', tokens['access_token'], tokens['refresh_token']) + + return tokens + + def authenticate_with_passcode(self, passcode): + """Performs IBM IAM SSO Authentication + + :param string passcode: your IBMid password + """ + + iam_client = requests.Session() + + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': consts.USER_AGENT, + 'Accept': 'application/json' + } + data = { + 'grant_type': 'urn:ibm:params:oauth:grant-type:passcode', + 'passcode': passcode, + 'response_type': 'cloud_iam' + } + + try: + response = iam_client.request( + 'POST', + 'https://iam.cloud.ibm.com/identity/token', + data=data, + headers=headers, + auth=requests.auth.HTTPBasicAuth('bx', 'bx') + ) + if response.status_code != 200: + LOGGER.error("Unable to login: %s", response.text) + + response.raise_for_status() + tokens = json.loads(response.text) + + except requests.HTTPError as ex: + error = json.loads(response.text) + raise exceptions.IAMError(response.status_code, + error.get('errorMessage'), + 'https://iam.cloud.ibm.com/identity/token') from ex + + self.settings['softlayer']['access_token'] = tokens['access_token'] + self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] + a_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['expiration'])) + r_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['refresh_token_expiration'])) + LOGGER.warning("Tokens retrieved, expires at %s, Refresh expires at %s", a_expire, r_expire) + config.write_config(self.settings, self.config_file) + self.auth = slauth.BearerAuthentication('', tokens['access_token'], tokens['refresh_token']) + + return tokens + + def authenticate_with_iam_token(self, a_token, r_token=None): + """Authenticates to the SL API with an IAM Token + + :param string a_token: Access token + :param string r_token: Refresh Token, to be used if Access token is expired. + """ + self.auth = slauth.BearerAuthentication('', a_token, r_token) + + def refresh_iam_token(self, r_token, account_id=None, ims_account=None): + """Refreshes the IAM Token, will default to values in the config file""" + iam_client = requests.Session() + + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': consts.USER_AGENT, + 'Accept': 'application/json' + } + data = { + 'grant_type': 'refresh_token', + 'refresh_token': r_token, + 'response_type': 'cloud_iam' + } + + sl_config = self.settings['softlayer'] + + if account_id is None and sl_config.get('account_id', False): + account_id = sl_config.get('account_id') + if ims_account is None and sl_config.get('ims_account', False): + ims_account = sl_config.get('ims_account') + + data['account'] = account_id + data['ims_account'] = ims_account + + try: + response = iam_client.request( + 'POST', + 'https://iam.cloud.ibm.com/identity/token', + data=data, + headers=headers, + auth=requests.auth.HTTPBasicAuth('bx', 'bx') + ) + + if response.status_code != 200: + LOGGER.warning("Unable to refresh IAM Token. %s", response.text) + + response.raise_for_status() + tokens = json.loads(response.text) + + except requests.HTTPError as ex: + error = json.loads(response.text) + raise exceptions.IAMError(response.status_code, + error.get('errorMessage'), + 'https://iam.cloud.ibm.com/identity/token') from ex + + a_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['expiration'])) + r_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['refresh_token_expiration'])) + LOGGER.warning("Tokens retrieved, expires at %s, Refresh expires at %s", a_expire, r_expire) + + self.settings['softlayer']['access_token'] = tokens['access_token'] + self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] + config.write_config(self.settings, self.config_file) + self.auth = slauth.BearerAuthentication('', tokens['access_token']) + return tokens + + def call(self, service, method, *args, **kwargs): + """Handles refreshing IAM tokens in case of a HTTP 401 error""" + try: + return super().call(service, method, *args, **kwargs) + except exceptions.SoftLayerAPIError as ex: + + if ex.faultCode == 401: + LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) + return ex + else: + raise ex + + 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 new file mode 100644 index 000000000..9f754b1d4 --- /dev/null +++ b/SoftLayer/CLI/account/billing_items.py @@ -0,0 +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.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 new file mode 100644 index 000000000..5b2b9b3df --- /dev/null +++ b/SoftLayer/CLI/account/cancel_item.py @@ -0,0 +1,24 @@ +"""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 5cc91144d..3dc59e329 100644 --- a/SoftLayer/CLI/account/events.py +++ b/SoftLayer/CLI/account/events.py @@ -2,53 +2,123 @@ # :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) - events = manager.get_upcoming_events() + 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) + add_ack_flag(unplanned_events, manager, ack_all) + add_ack_flag(announcement_events, manager, ack_all) + + 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): + """Add acknowledgedFlag to the event""" if ack_all: for event in events: result = manager.ack_event(event['id']) event['acknowledgedFlag'] = result - env.fout(event_table(events)) -def event_table(events): +def planned_event_table(events): """Formats a table for events""" - table = formatting.Table([ - "Id", - "Start Date", - "End Date", - "Subject", - "Status", - "Acknowledged", - "Updates", - "Impacted Resources" - ], title="Upcoming Events") - table.align['Subject'] = 'l' - table.align['Impacted Resources'] = 'l' + planned_table = formatting.Table(['Event Data', 'Id', 'Event ID', 'Subject', 'Status', 'Items', 'Start Date', + 'End Date', 'Acknowledged', 'Updates'], title="Planned Events") + planned_table.align['Subject'] = 'l' + planned_table.align['Impacted Resources'] = 'l' for event in events: - table.add_row([ + planned_table.add_row([ + utils.clean_time(event.get('startDate')), event.get('id'), + event.get('systemTicketId'), + # Some subjects can have \r\n for some reason. + utils.clean_splitlines(event.get('subject')), + utils.lookup(event, 'statusCode', 'name'), + event.get('impactedResourceCount'), utils.clean_time(event.get('startDate')), utils.clean_time(event.get('endDate')), + event.get('acknowledgedFlag'), + event.get('updateCount'), + + ]) + return planned_table + + +def unplanned_event_table(events): + """Formats a table for events""" + unplanned_table = formatting.Table(['Id', 'Event ID', 'Subject', 'Status', 'Items', 'Start Date', + 'Last Updated', 'Acknowledged', 'Updates'], title="Unplanned Events") + unplanned_table.align['Subject'] = 'l' + unplanned_table.align['Impacted Resources'] = 'l' + for event in events: + unplanned_table.add_row([ + event.get('id'), + event.get('systemTicketId'), # Some subjects can have \r\n for some reason. utils.clean_splitlines(event.get('subject')), utils.lookup(event, 'statusCode', 'name'), + event.get('impactedResourceCount'), + utils.clean_time(event.get('startDate')), + utils.clean_time(event.get('modifyDate')), event.get('acknowledgedFlag'), event.get('updateCount'), - event.get('impactedResourceCount') ]) - return table + return unplanned_table + + +def announcement_event_table(events): + """Formats a table for events""" + announcement_table = formatting.Table( + ['Id', 'Event ID', 'Subject', 'Status', 'Items', 'Acknowledged', 'Updates'], title="Announcement Events") + announcement_table.align['Subject'] = 'l' + announcement_table.align['Impacted Resources'] = 'l' + for event in events: + announcement_table.add_row([ + event.get('id'), + event.get('systemTicketId'), + # Some subjects can have \r\n for some reason. + utils.clean_splitlines(event.get('subject')), + utils.lookup(event, 'statusCode', 'name'), + event.get('impactedResourceCount'), + event.get('acknowledgedFlag'), + event.get('updateCount') + ]) + return announcement_table 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 b840f3f60..4436c44d9 100644 --- a/SoftLayer/CLI/account/invoice_detail.py +++ b/SoftLayer/CLI/account/invoice_detail.py @@ -3,22 +3,45 @@ 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) + table = get_invoice_table(identifier, top_items, details) + env.fout(table) + + +def nice_string(ugly_string, limit=100): + """Format and trims strings""" + return (ugly_string[:limit] + '..') if len(ugly_string) > limit else ugly_string + + +def get_invoice_table(identifier, top_items, details): + """Formats a table for invoice top level items. + + :param int identifier: Invoice identifier. + :param list top_items: invoiceTopLevelItems. + :param bool details: To add very detailed list of charges. + """ title = "Invoice %s" % identifier table = formatting.Table(["Item Id", "Category", "Description", "Single", @@ -32,16 +55,31 @@ def cli(env, identifier, 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([ '>>>', @@ -52,10 +90,17 @@ def cli(env, identifier, details): '---', '---' ]) + return table - env.fout(table) +def sum_item_charges(item: dict) -> (float, float): + """Takes a billing Item, sums up its child items and returns recurring, one_time prices""" -def nice_string(ugly_string, limit=100): - """Format and trims strings""" - return (ugly_string[:limit] + '..') if len(ugly_string) > limit else ugly_string + # 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 new file mode 100644 index 000000000..a67ed6f65 --- /dev/null +++ b/SoftLayer/CLI/account/item_detail.py @@ -0,0 +1,54 @@ +"""Gets some details about a specific 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.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + + +@click.command(cls=SLCommand) +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Gets detailed information about a billing item.""" + manager = AccountManager(env.client) + item = manager.get_item_detail(identifier) + env.fout(item_table(item)) + + +def item_table(item): + """Formats a table for billing items""" + + date_format = '%Y-%m-%d' + 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 = f"{item.get('hostName', '')}.{item.get('domainName', '')}" + if fqdn != ".": + table.add_row(['FQDN', fqdn]) + + if item.get('hourlyFlag', False): + table.add_row(['hourlyRecurringFee', item.get('hourlyRecurringFee')]) + table.add_row(['hoursUsed', item.get('hoursUsed')]) + table.add_row(['currentHourlyCharge', item.get('currentHourlyCharge')]) + else: + table.add_row(['recurringFee', item.get('recurringFee')]) + + ordered_by = "IBM" + user = utils.lookup(item, 'orderItem', 'order', 'userRecord') + if user: + 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')]) + if item.get('children'): + for child in item.get('children'): + table.add_row([child.get('categoryCode'), child.get('description')]) + + return table diff --git a/SoftLayer/CLI/account/licenses.py b/SoftLayer/CLI/account/licenses.py new file mode 100644 index 000000000..7b328c7dd --- /dev/null +++ b/SoftLayer/CLI/account/licenses.py @@ -0,0 +1,43 @@ +"""Show all licenses.""" +# :license: MIT, see LICENSE for more details. +import click + +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(cls=SLCommand) +@environment.pass_env +def cli(env): + """Show all licenses.""" + + manager = account.AccountManager(env.client) + + control_panel = manager.get_active_virtual_licenses() + vmwares = manager.get_active_account_licenses() + + table_panel = formatting.KeyValueTable(['id', 'ip_address', 'manufacturer', 'software', + 'key', 'subnet', 'subnet notes'], title="Control Panel Licenses") + + table_vmware = formatting.KeyValueTable(['name', 'license_key', 'cpus', 'description', + 'manufacturer', 'requiredUser'], title="VMware Licenses") + for panel in control_panel: + table_panel.add_row([panel.get('id'), panel.get('ipAddress'), + utils.lookup(panel, 'softwareDescription', 'manufacturer'), + utils.trim_to(utils.lookup(panel, 'softwareDescription', 'longDescription'), 40), + panel.get('key'), utils.lookup(panel, 'subnet', 'broadcastAddress'), + utils.lookup(panel, 'subnet', 'note')]) + + env.fout(table_panel) + for vmware in vmwares: + table_vmware.add_row([utils.lookup(vmware, 'softwareDescription', 'name'), + vmware.get('key'), vmware.get('capacity'), + utils.lookup(vmware, 'billingItem', 'description'), + utils.lookup(vmware, 'softwareDescription', 'manufacturer'), + utils.lookup(vmware, 'softwareDescription', 'requiredUser')]) + + env.fout(table_vmware) diff --git a/SoftLayer/CLI/account/orders.py b/SoftLayer/CLI/account/orders.py new file mode 100644 index 000000000..f4cc1fa8b --- /dev/null +++ b/SoftLayer/CLI/account/orders.py @@ -0,0 +1,56 @@ +"""Lists account orders.""" +# :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 + + +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, 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") + order_table.align = 'l' + + for order in orders: + items = [] + for item in order['items']: + items.append(item['description']) + create_date = utils.clean_time(order['createDate'], in_format='%Y-%m-%d', out_format='%Y-%m-%d') + + 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/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 c470aebbf..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') 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 b011e263a..c2aa4c243 100644 --- a/SoftLayer/CLI/block/access/list.py +++ b/SoftLayer/CLI/block/access/list.py @@ -6,23 +6,32 @@ from SoftLayer.CLI import columns as column_helper from SoftLayer.CLI import environment from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers 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( - volume_id=volume_id) + volume_id=resolved_id) table = formatting.Table(columns.columns) table.sortby = sortby 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 new file mode 100644 index 000000000..2df175c3f --- /dev/null +++ b/SoftLayer/CLI/block/convert.py @@ -0,0 +1,17 @@ +"""Convert a dependent duplicate volume to an independent 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): + """Convert a dependent duplicate volume to an independent volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + resp = block_manager.convert_dep_dupe(volume_id) + + click.echo(resp) diff --git a/SoftLayer/CLI/block/count.py b/SoftLayer/CLI/block/count.py index dc4fb89c1..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 @@ -25,18 +25,18 @@ def cli(env, sortby, datacenter): mask=mask) # cycle through all block volumes and count datacenter occurences. - datacenters = dict() + datacenters = {} for volume in block_volumes: 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 table = formatting.KeyValueTable(DEFAULT_COLUMNS) table.sortby = sortby - for datacenter_name in datacenters: - table.add_row([datacenter_name, datacenters[datacenter_name]]) + for key, value in datacenters.items(): + table.add_row([key, value]) env.fout(table) diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py index 6b1b7288c..a4359fae3 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -5,99 +5,78 @@ import SoftLayer from SoftLayer.CLI import environment from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers 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): """Display details for a specified volume.""" block_manager = SoftLayer.BlockStorageManager(env.client) - block_volume = block_manager.get_block_volume_details(volume_id) + block_volume_id = helpers.resolve_id(block_manager.resolve_ids, volume_id, 'Block Volume') + block_volume = block_manager.get_block_volume_details(block_volume_id) block_volume = utils.NestedDict(block_volume) table = formatting.KeyValueTable(['Name', 'Value']) 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']) @@ -108,4 +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 = 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 ec728f87c..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, @@ -54,10 +54,23 @@ type=click.Choice(['hourly', 'monthly']), default='monthly', help="Optional parameter for Billing rate (default to monthly)") +@click.option('--dependent-duplicate', + type=click.BOOL, + default=False, + show_default=True, + help='Whether or not this duplicate will be a dependent duplicate ' + 'of the origin volume.') @environment.pass_env def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size, - duplicate_iops, duplicate_tier, duplicate_snapshot_size, billing): - """Order a duplicate block storage volume.""" + duplicate_iops, duplicate_tier, duplicate_snapshot_size, billing, + dependent_duplicate): + """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 @@ -75,14 +88,14 @@ def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size, duplicate_iops=duplicate_iops, duplicate_tier_level=duplicate_tier, duplicate_snapshot_size=duplicate_snapshot_size, - hourly_billing_flag=hourly_billing_flag + hourly_billing_flag=hourly_billing_flag, + dependent_duplicate=dependent_duplicate ) 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']) 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 new file mode 100644 index 000000000..6af71c9e3 --- /dev/null +++ b/SoftLayer/CLI/block/limit.py @@ -0,0 +1,50 @@ +"""List number of block storage volumes limit per datacenter.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +DEFAULT_COLUMNS = [ + 'Datacenter', + 'MaximumAvailableCount', + 'ProvisionedCount' +] + + +@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, 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 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 948e6c127..9a65603e0 100644 --- a/SoftLayer/CLI/block/list.py +++ b/SoftLayer/CLI/block/list.py @@ -5,8 +5,7 @@ import SoftLayer from SoftLayer.CLI import columns as column_helper from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - +from SoftLayer.CLI import storage_utils COLUMNS = [ column_helper.Column('id', ('id',), mask="id"), @@ -18,12 +17,11 @@ 'storage_type', lambda b: b['storageType']['keyName'].split('_').pop(0) if 'storageType' in b and 'keyName' in b['storageType'] - and isinstance(b['storageType']['keyName'], str) + and isinstance(b['storageType']['keyName'], str) else '-', mask="storageType.keyName"), column_helper.Column('capacity_gb', ('capacityGb',), mask="capacityGb"), - column_helper.Column('bytes_used', ('bytesUsed',), mask="bytesUsed"), - column_helper.Column('iops', ('iops',), mask="iops"), + column_helper.Column('IOPs', ('provisionedIops',), mask="provisionedIops"), column_helper.Column('ip_addr', ('serviceResourceBackendIpAddress',), mask="serviceResourceBackendIpAddress"), column_helper.Column('lunId', ('lunId',), mask="lunId"), @@ -34,6 +32,7 @@ column_helper.Column( 'created_by', ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), + column_helper.Column('notes', ('notes',), mask="notes"), ] DEFAULT_COLUMNS = [ @@ -42,41 +41,43 @@ 'datacenter', 'storage_type', 'capacity_gb', - 'bytes_used', - 'iops', + 'IOPs', 'ip_addr', 'lunId', 'active_transactions', - 'rep_partner_count' + 'rep_partner_count', + 'notes' ] -@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') @click.option('--storage-type', help='Type of storage volume', type=click.Choice(['performance', 'endurance'])) @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): - """List block storage.""" +def cli(env, sortby, columns, datacenter, username, storage_type, order): + """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, storage_type=storage_type, + order=order, mask=columns.mask()) - table = formatting.Table(columns.columns) - table.sortby = sortby - - for block_volume in block_volumes: - table.add_row([value or formatting.blank() - for value in columns.row(block_volume)]) - + table = storage_utils.build_output_table(env, block_volumes, columns, sortby) env.fout(table) 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 302b45cc8..bdd0100e0 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -6,11 +6,10 @@ from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions - 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']), @@ -63,7 +62,7 @@ def cli(env, storage_type, size, iops, tier, os_type, """Order a block storage volume. Valid size and iops options can be found here: - https://console.bluemix.net/docs/infrastructure/BlockStorage/index.html#provisioning + https://cloud.ibm.com/docs/BlockStorage/index.html#provisioning-considerations """ block_manager = SoftLayer.BlockStorageManager(env.client) storage_type = storage_type.lower() @@ -73,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') @@ -124,9 +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(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 new file mode 100644 index 000000000..e58fbf28d --- /dev/null +++ b/SoftLayer/CLI/block/refresh.py @@ -0,0 +1,26 @@ +"""Refresh a duplicate volume with a snapshot from its parent.""" +# :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.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, 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, 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 new file mode 100644 index 000000000..000263525 --- /dev/null +++ b/SoftLayer/CLI/block/replication/disaster_recovery_failover.py @@ -0,0 +1,45 @@ +"""Failover an inaccessible block volume to its available replicant volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, + 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.") +@environment.pass_env +def cli(env, volume_id, replicant_id): + """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.""" + """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 the API.""" + """To failback to the original volume after using this method, open a support ticket.""" + """If you wish to test failover, use replica-failover instead.""", fg='red') + + if not formatting.confirm('Are you sure you want to continue?'): + raise exceptions.CLIAbort('Aborted.') + + block_storage_manager.disaster_recovery_failover_to_replicant( + volume_id, + replicant_id + ) + + click.echo("Disaster Recovery Failover to replicant is now in progress.") 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 743c91c0e..e3c666f2a 100644 --- a/SoftLayer/CLI/block/replication/order.py +++ b/SoftLayer/CLI/block/replication/order.py @@ -5,29 +5,25 @@ import SoftLayer from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers +from SoftLayer import utils 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, ' - '(INTERVAL | HOURLY | DAILY | WEEKLY)', - required=True, - type=click.Choice(['INTERVAL', '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', @@ -36,30 +32,47 @@ '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') 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( - volume_id, + 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( - order['placedOrder']['id'])) - for item in order['placedOrder']['items']: - click.echo(" > %s" % item['description']) + 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: click.echo("Order could not be placed! Please verify your options " + "and try again.") diff --git a/SoftLayer/CLI/block/replication/partners.py b/SoftLayer/CLI/block/replication/partners.py index f19be0af5..c5ae5075b 100644 --- a/SoftLayer/CLI/block/replication/partners.py +++ b/SoftLayer/CLI/block/replication/partners.py @@ -6,39 +6,22 @@ from SoftLayer.CLI import columns as column_helper from SoftLayer.CLI import environment from SoftLayer.CLI import formatting +from SoftLayer.CLI import storage_utils -COLUMNS = [ - column_helper.Column('ID', ('id',)), - column_helper.Column('Username', ('username',), mask="username"), - column_helper.Column('Account ID', ('accountId',), mask="accountId"), - column_helper.Column('Capacity (GB)', ('capacityGb',), mask="capacityGb"), - column_helper.Column('Hardware ID', ('hardwareId',), mask="hardwareId"), - column_helper.Column('Guest ID', ('guestId',), mask="guestId"), - column_helper.Column('Host ID', ('hostId',), mask="hostId"), -] +COLUMNS = storage_utils.REPLICATION_PARTNER_COLUMNS +DEFAULT_COLUMNS = storage_utils.REPLICATION_PARTNER_DEFAULT -DEFAULT_COLUMNS = [ - 'ID', - 'Username', - 'Account ID', - 'Capacity (GB)', - 'Hardware ID', - 'Guest ID', - 'Host ID' -] - -@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 new file mode 100644 index 000000000..8c35762ca --- /dev/null +++ b/SoftLayer/CLI/block/set_note.py @@ -0,0 +1,33 @@ +"""Set note for an existing block storage volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume-id') +@click.option('--note', '-n', + type=str, + required=True, + 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. + + 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') + + result = block_manager.volume_set_note(block_volume_id, note) + + if result: + click.echo("Set note successfully!") + + else: + click.echo("Note could not be set! Please verify your options and try again.") 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 b47f5949f..e9eca5983 100644 --- a/SoftLayer/CLI/block/snapshot/list.py +++ b/SoftLayer/CLI/block/snapshot/list.py @@ -6,6 +6,7 @@ from SoftLayer.CLI import columns as column_helper from SoftLayer.CLI import environment from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers COLUMNS = [ @@ -25,21 +26,21 @@ ] -@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): """List block storage snapshots.""" block_manager = SoftLayer.BlockStorageManager(env.client) + resolved_id = helpers.resolve_id(block_manager.resolve_ids, volume_id, 'Volume Id') snapshots = block_manager.get_block_volume_snapshot_list( - volume_id, + resolved_id, mask=columns.mask() ) 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/__init__.py b/SoftLayer/CLI/block/subnets/__init__.py new file mode 100644 index 000000000..8824c4279 --- /dev/null +++ b/SoftLayer/CLI/block/subnets/__init__.py @@ -0,0 +1 @@ +"""Block Storage Subnets Control.""" diff --git a/SoftLayer/CLI/block/subnets/assign.py b/SoftLayer/CLI/block/subnets/assign.py new file mode 100644 index 000000000..d80e5aff4 --- /dev/null +++ b/SoftLayer/CLI/block/subnets/assign.py @@ -0,0 +1,43 @@ +"""Assign block storage subnets to the given host id.""" +# :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('access_id', type=int) +@click.option('--subnet-id', multiple=True, type=int, + help="ID of the subnets to assign; e.g.: --subnet-id 1234") +@environment.pass_env +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 + """ + try: + subnet_ids = list(subnet_id) + block_manager = SoftLayer.BlockStorageManager(env.client) + assigned_subnets = block_manager.assign_subnets_to_acl(access_id, subnet_ids) + + for subnet in assigned_subnets: + 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 = f"Failed to assign subnet id: {subnet} to allowed host id: {access_id}" + click.echo(message) + + except SoftLayer.SoftLayerAPIError as ex: + 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 new file mode 100644 index 000000000..297846abd --- /dev/null +++ b/SoftLayer/CLI/block/subnets/list.py @@ -0,0 +1,48 @@ +"""List block storage assigned subnets for the given host id.""" +# :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 + + +COLUMNS = [ + 'id', + 'networkIdentifier', + 'cidr' +] + + +@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 + """ + + try: + block_manager = SoftLayer.BlockStorageManager(env.client) + resolved_id = helpers.resolve_id(block_manager.resolve_ids, access_id, 'Volume Id') + subnets = block_manager.get_subnets_in_acl(resolved_id) + + table = formatting.Table(COLUMNS) + for subnet in subnets: + row = [f"{subnet['id']}", + f"{subnet['networkIdentifier']}", + f"{subnet['cidr']}"] + table.add_row(row) + + env.fout(table) + + except SoftLayer.SoftLayerAPIError as ex: + message = "Unable to list assigned subnets for access-id: {}.\nReason: {}".format(access_id, ex.faultString) + click.echo(message) diff --git a/SoftLayer/CLI/block/subnets/remove.py b/SoftLayer/CLI/block/subnets/remove.py new file mode 100644 index 000000000..98d52ad11 --- /dev/null +++ b/SoftLayer/CLI/block/subnets/remove.py @@ -0,0 +1,43 @@ +"""Remove block storage subnets for the given host id.""" +# :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('access_id', type=int) +@click.option('--subnet-id', multiple=True, type=int, + help="ID of the subnets to remove; e.g.: --subnet-id 1234") +@environment.pass_env +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 + """ + try: + subnet_ids = list(subnet_id) + block_manager = SoftLayer.BlockStorageManager(env.client) + removed_subnets = block_manager.remove_subnets_from_acl(access_id, subnet_ids) + + for subnet in removed_subnets: + 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 = f"Failed to remove subnet id: {subnet} for allowed host id: {access_id}" + click.echo(message) + + except SoftLayer.SoftLayerAPIError as ex: + 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 6e16a2a77..e6c007e25 100644 --- a/SoftLayer/CLI/call_api.py +++ b/SoftLayer/CLI/call_api.py @@ -1,6 +1,9 @@ """Call arbitrary API endpoints.""" +import json + 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 @@ -27,8 +30,7 @@ def _build_filters(_filters): if len(top_parts) == 2: break else: - raise exceptions.CLIAbort('Failed to find valid operation for: %s' - % _filter) + raise exceptions.CLIAbort('Failed to find valid operation for: %s' % _filter) key, value = top_parts current = root @@ -68,25 +70,66 @@ def _build_python_example(args, kwargs): return call_str -@click.command('call', short_help="Call arbitrary API endpoints.") +def _validate_filter(ctx, param, value): # pylint: disable=unused-argument + """Validates a JSON style object filter""" + _filter = None + if value: + try: + _filter = json.loads(value) + if not isinstance(_filter, dict): + raise exceptions.CLIAbort(f"\"{_filter}\" should be a JSON object, but is a {type(_filter)} instead.") + except json.JSONDecodeError as error: + raise exceptions.CLIAbort(f"\"{value}\" is not valid JSON. {error}") + + return _filter + + +def _validate_parameters(ctx, param, value): # pylint: disable=unused-argument + """Checks if value is a JSON string, and converts it to a datastructure if that is true""" + + validated_values = [] + for parameter in value: + if isinstance(parameter, str): + # looks like a JSON string... + if '{' in parameter or '[' in parameter: + try: + parameter = json.loads(parameter) + except json.JSONDecodeError as error: + 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.", cls=SLCommand) @click.argument('service') @click.argument('method') -@click.argument('parameters', nargs=-1) +@click.argument('parameters', nargs=-1, callback=_validate_parameters) @click.option('--id', '_id', help="Init parameter") @helpers.multi_option('--filter', '-f', '_filters', - help="Object filters. This should be of the form: " - "'property=value' or 'nested.property=value'. Complex " - "filters like betweenDate are not currently supported.") + help="Object filters. This should be of the form: 'property=value' or 'nested.property=value'." + "Complex filters should use --json-filter.") @click.option('--mask', help="String-based object mask") @click.option('--limit', type=click.INT, help="Result limit") @click.option('--offset', type=click.INT, help="Result offset") +@click.option('--orderBy', type=click.STRING, + help="To set the sort direction, ASC or DESC can be provided." + "This should be of the form: '--orderBy nested.property' default DESC or " + "'--orderBy nested.property=ASC', e.g. " + " --orderBy subnets.id default DESC" + " --orderBy subnets.id=ASC") @click.option('--output-python / --no-output-python', help="Show python example code instead of executing the call") +@click.option('--json-filter', callback=_validate_filter, + help="A JSON string to be passed in as the object filter to the API call. " + "Remember to use double quotes (\") for variable names. Can NOT be used with --filter. " + "Dont use whitespace outside of strings, or the slcli might have trouble parsing it.") @environment.pass_env -def cli(env, service, method, parameters, _id, _filters, mask, limit, offset, - output_python=False): +def cli(env, service, method, parameters, _id, _filters, mask, limit, offset, orderby=None, + output_python=False, json_filter=None): """Call arbitrary API endpoints with the given SERVICE and METHOD. + For parameters that require a datatype, use a JSON string for that parameter. Example:: slcli call-api Account getObject @@ -100,19 +143,38 @@ def cli(env, service, method, parameters, _id, _filters, mask, limit, offset, --mask=id,hostname,datacenter.name,maxCpu slcli call-api Account getVirtualGuests \\ -f 'virtualGuests.datacenter.name IN dal05,sng01' + slcli call-api Account getVirtualGuests \\ + --json-filter '{"virtualGuests":{"hostname":{"operation":"^= test"}}}' --limit=10 + slcli -v call-api SoftLayer_User_Customer addBulkPortalPermission --id=1234567 \\ + '[{"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: + raise exceptions.CLIAbort("--filter and --json-filter cannot be used together.") + + object_filter = _build_filters(_filters) + if orderby: + orderby = utils.build_filter_orderby(orderby) + object_filter = utils.dict_merge(object_filter, orderby) + if json_filter: + object_filter = utils.dict_merge(json_filter, object_filter) + args = [service, method] + list(parameters) kwargs = { 'id': _id, - 'filter': _build_filters(_filters), + 'filter': object_filter, 'mask': mask, 'limit': limit, 'offset': offset, } 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/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