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 ebf9932d3..5dd1975be 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ dist/* *.egg-info .cache .idea +.pytest_cache/* 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 new file mode 100644 index 000000000..18d147019 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,30 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.10" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + builder: htmldir + configuration: docs/conf.py + +# Build documentation with MkDocs +#mkdocs: +# configuration: mkdocs.yml + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: all + +# Optionally set the version of Python and requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 000000000..394061815 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,770 @@ +{ + "exclude": { + "files": "^.secrets.baseline$", + "lines": null + }, + "generated_at": "2025-02-14T20:05:29Z", + "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": 181, + "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 c75b2b644..000000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: python -sudo: false -matrix: - include: - - python: "2.7" - env: TOX_ENV=py27 - - python: "3.3" - env: TOX_ENV=py33 - - python: "3.4" - env: TOX_ENV=py34 - - python: "3.5" - env: TOX_ENV=py35 - - python: "3.6" - env: TOX_ENV=py36 - - python: "pypy2.7-5.8.0" - env: TOX_ENV=pypy - - python: "2.7" - env: TOX_ENV=analysis - - python: "2.7" - 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 7c027e63b..abee1bea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,765 @@ # Change Log +## [6.1.6] - 2023-03-27 + +From now on changes will be published only on GitHub https://github.com/softlayer/softlayer-python/releases + + +## [6.1.3] - 2022-11-30 + +#### What's Changed +* New Command: Hardware notifications by @caberos in https://github.com/softlayer/softlayer-python/pull/1756 +* New Command: virtual notifications by @caberos in https://github.com/softlayer/softlayer-python/pull/1758 +* Change regex in rich text in simple option in help text by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1759 +* pip prod(deps): bump rich from 12.5.1 to 12.6.0 by @dependabot in https://github.com/softlayer/softlayer-python/pull/1760 +* add more information to vs credentials by @caberos in https://github.com/softlayer/softlayer-python/pull/1762 +* Fixed maxint issue by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1765 +* Added csv output format by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1766 +* add more information on hw credentials by @caberos in https://github.com/softlayer/softlayer-python/pull/1767 +* Delete twitter link in documentation by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1769 +* new command hw create-credential by @caberos in https://github.com/softlayer/softlayer-python/pull/1774 +* fix the hw credential error by @caberos in https://github.com/softlayer/softlayer-python/pull/1781 +* Added test suite for py311 by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1784 +* New feature to change theme in slcli like dark, light o maintain in default by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1775 +* Match `virtual detail --prices` option with `hardware detail --prices` option by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1780 +* Fixing preset-list pricing table by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1782 +* fix the call api cannot handle empty results by @caberos in https://github.com/softlayer/softlayer-python/pull/1789 +* Debug output changed to a valid JSON by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1791 +* slcli hw add-notifications crashes with a bad user by @caberos in https://github.com/softlayer/softlayer-python/pull/1786 +* del-notification commands, rename the commands to notifications-add by @caberos in https://github.com/softlayer/softlayer-python/pull/1785 +* Add --extras to slcli order quote by @caberos in https://github.com/softlayer/softlayer-python/pull/1792 +* An error is displaying for volume with replica in slcli for Active pr… by @caberos in https://github.com/softlayer/softlayer-python/pull/1794 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.1.2...v6.1.3 + + +## [6.1.2] - 2022-09-23 + +#### What's Changed +* Snapcraft: Updated to Core22 and add homeishome-launch by @kz6fittycent in https://github.com/softlayer/softlayer-python/pull/1740 +* Add status, create date and domain columns in `slcli vs list command` by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1728 +* New command: ipsec cancel by @caberos in https://github.com/softlayer/softlayer-python/pull/1729 +* New command: subnet clear-route by @caberos in https://github.com/softlayer/softlayer-python/pull/1738 +* Deprecate slcli hw guests by @caberos in https://github.com/softlayer/softlayer-python/pull/1736 +* Remove real usersnames from test fixtrues by @caberos in https://github.com/softlayer/softlayer-python/pull/1743 +* Fix tox request.get hangout issue by @caberos in https://github.com/softlayer/softlayer-python/pull/1746 +* add vs user-access command by @caberos in https://github.com/softlayer/softlayer-python/pull/1741 +* Update Help message for commands that take in multiple arguments by @caberos in https://github.com/softlayer/softlayer-python/pull/1748 +* Error with slcli order item-list by @caberos in https://github.com/softlayer/softlayer-python/pull/1751 +* deprecate sl `autoscale` by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1753 +* Unhandled error running a subcommand in slcli by @caberos in https://github.com/softlayer/softlayer-python/pull/1754 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.1.1...v6.1.2 + + +## [6.1.1] - 2022-08-18 + +#### What's Changed +* v6.1.0 Changelog and version bump by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1674 +* item-list fix by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1679 +* updating release job to actually publish to pypi by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1680 +* Update command - slcli object-storage endpoints by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1685 +* add the block volume-options command by @caberos in https://github.com/softlayer/softlayer-python/pull/1681 +* add the file volume-options command by @caberos in https://github.com/softlayer/softlayer-python/pull/1684 +* fixed issues where a message warned users about closing datacenter by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1688 +* Enable --format=raw and fixes table width by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1689 +* Update `slcli hardware sensor` by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1691 +* Improved successful response to command - slcli vs cancel by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1695 +* Fixed an issue with printing tables that contained empty items by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1697 +* Added a dependabot scanner by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1699 +* block|file volume-options improvements by @caberos in https://github.com/softlayer/softlayer-python/pull/1700 +* Option create-options in commands hardware and dedicatedhost fixed by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1703 +* pip prod(deps): bump rich from 12.3.0 to 12.5.1 by @dependabot in https://github.com/softlayer/softlayer-python/pull/1704 +* block/file volume-options improvements 2 by @caberos in https://github.com/softlayer/softlayer-python/pull/1702 +* New command ipsec order by @caberos in https://github.com/softlayer/softlayer-python/pull/1698 +* block/file volume-options improvement 3 by @caberos in https://github.com/softlayer/softlayer-python/pull/1705 +* Command slcli vlan create - displaying an error message by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1707 +* New Command: user device-access by @caberos in https://github.com/softlayer/softlayer-python/pull/1712 +* Command slcli vlan edit accept that we do not send any parameters by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1709 +* Updated command - slcli vlan list by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1713 +* `slcli block subnets-list` command display an error message by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1716 +* add user remove-access command by @caberos in https://github.com/softlayer/softlayer-python/pull/1717 +* Add Devices with Trunks to vlan detail by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1721 +* slcli hardware reflash-firmware command does not display success message by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1724 +* Fix bug with command - slcli cdn edit by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1726 + +#### New Contributors +* @dependabot made their first contribution in https://github.com/softlayer/softlayer-python/pull/1704 + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.1.0...v6.1.1 + + + +## [6.1.0] - 2022-06-30 + +#### Major Updates +* [Rich](https://github.com/Textualize/rich) tables by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1646 +* [Rich](https://github.com/Textualize/rich) Text support by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1635 + +Rich Text and Rich Tables will modernize the output of the SLCLI to be a little nicer to look at, with colors and other highlighting. + +![image](https://user-images.githubusercontent.com/7408017/176753783-f6a4a43a-53ac-4600-a24f-21362f152747.png) +![image](https://user-images.githubusercontent.com/7408017/176753845-32af33f0-454f-4bab-ac63-1ae3db788ede.png) + + +#### What's Changed +* slcli licenses is missing the help text by @caberos in https://github.com/softlayer/softlayer-python/pull/1605 +* Add a warning if user orders in a POD that is being closed by @caberos in https://github.com/softlayer/softlayer-python/pull/1600 +* updated number of updates in the command account event-detail by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1609 +* Add an orderBy filter to slcli vlan list by @caberos in https://github.com/softlayer/softlayer-python/pull/1599 +* Add options to print a specific table in command slcli account events by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1611 +* Update global ip assign/unassign to use new API by @caberos in https://github.com/softlayer/softlayer-python/pull/1614 +* Ability to route/unroute subnets by @caberos in https://github.com/softlayer/softlayer-python/pull/1615 +* Improved successful response to command - slcli account cancel-item by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1617 +* Improved successful response to command - slcli virtual edit by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1618 +* Improved successful response to command - slcli vlan cancel by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1619 +* Mishandling of domain and hostname data in `slcli account item-detail` by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1621 +* Unable to get VSI details when last TXN is "Software install is finis… by @caberos in https://github.com/softlayer/softlayer-python/pull/1625 +* new command on autoscale delete by @caberos in https://github.com/softlayer/softlayer-python/pull/1628 +* Incorrect table title is displayed when an Auto Scale Group is scaled to reduce members by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1629 +* slcli autoscale create by @caberos in https://github.com/softlayer/softlayer-python/pull/1623 +* Soap transport by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1626 +* fix issue on loadbal order command by @caberos in https://github.com/softlayer/softlayer-python/pull/1633 +* Policy is not added when an AutoScale Group is created by @caberos in https://github.com/softlayer/softlayer-python/pull/1637 +* When `slcli event-log` not return any event log the command display an error by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1641 +* add new columns on vlan list(premium, tags) by @caberos in https://github.com/softlayer/softlayer-python/pull/1645 +* fixed documentation build issues by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1648 +* Improved successful response to command - slcli licenses cancel by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1653 +* update the firewall list by @caberos in https://github.com/softlayer/softlayer-python/pull/1649 +* Updated readme by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1656 +* Update `slcli firewall detail` to handle multi vlan firewalls by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1651 +* New command for getting duplicate convert status by @ko101 in https://github.com/softlayer/softlayer-python/pull/1655 +* Fixed TOX errors by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1661 +* add a new feature to get all cloud object storage by @caberos in https://github.com/softlayer/softlayer-python/pull/1662 +* Update `slcli report bandwidth` command by @BrianSantivanez in https://github.com/softlayer/softlayer-python/pull/1664 +* add firewall monitoring command by @caberos in https://github.com/softlayer/softlayer-python/pull/1657 +* add a new command on block object-storage details by @caberos in https://github.com/softlayer/softlayer-python/pull/1666 +* slcli account bandwidth-pools-detail command displays an error with b… by @caberos in https://github.com/softlayer/softlayer-python/pull/1670 +* new feature block object-storage permissions command by @caberos in https://github.com/softlayer/softlayer-python/pull/1668 +* fix the vlan table by @caberos in https://github.com/softlayer/softlayer-python/pull/1672 + +#### New Contributors +* @BrianSantivanez made their first contribution in https://github.com/softlayer/softlayer-python/pull/1629 + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.0.2...v6.1.0 + + +### [6.0.2] - 2022-03-30 + +#### What's Changed +* New Command slcli hardware|virtual monitoring by @caberos in https://github.com/softlayer/softlayer-python/pull/1593 +* When listing datacenters/pods, mark those that are closing soon. by @caberos in https://github.com/softlayer/softlayer-python/pull/1597 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v6.0.1...v6.0.2 + +## [6.0.1] - 2022-03-11 + + +#### What's Changed +* Replace the use of ptable with prettytable by @dvzrv in https://github.com/softlayer/softlayer-python/pull/1584 +* Bandwidth pool management by @caberos in https://github.com/softlayer/softlayer-python/pull/1582 +* Add id in the result in the command bandwidth-pools by @edsonarios in https://github.com/softlayer/softlayer-python/pull/1586 +* Datacenter closure report by @allmightyspiff in https://github.com/softlayer/softlayer-python/pull/1592 +* fix to errors in slcli hw create-options by @caberos in https://github.com/softlayer/softlayer-python/pull/1594 + + +**Full Changelog**: https://github.com/softlayer/softlayer-python/compare/v5.9.9...v6.0.1 + +6.0.0 was skipped. + +## [5.9.9] - 2022-02-04 + +https://github.com/softlayer/softlayer-python/compare/v5.9.8...v5.9.9 + +#### Improvements +- Add loadbalancer timeout values #1576 +- Add pricing date to slcli order preset-list #1578 + +#### New Commands +- `slcli vlan create-options` add new feature on vlan #1572 +- `slcli account bandwidth-pools` Bandwidth pool features #1579 + +## [5.9.8] - 2021-12-07 + +https://github.com/softlayer/softlayer-python/compare/v5.9.7...v5.9.8 + +#### Improvements + +- Fix code blocks formatting of The Solution section docs #1534 +- Add retry decorator to documentation #1535 +- Updated utility docs #1536 +- Add Exceptions to Documentation #1537 +- Forces specific encoding on XMLRPC requests #1543 +- Add sensor data to hardware #1544 +- Ignoring f-string related messages for tox for now #1548 +- Fix account events #1546 +- Improved loadbal details #1549 +- Fix initialized accountmanger #1552 +- Fix hw billing reports 0 items #1556 +- Update API docs link and remove travisCI mention #1557 +- Fix errors with vs bandwidth #1563 +- Add Item names to vs billing report #1564 +- Mapping is now in collections.abc #1565 +- fix vs placementgroup list #1567 +- fixed up snapshot-notification cli commands #1569 + +#### New Commands +- loadbal l7policies #1553 + + ` slcli loadbal l7policies --protocol-id` + + `slcli loadbal l7policies` +- Snapshot notify #1554 + + `slcli file|block snapshot-set-notification` + + `slcli file|block snapshot-get-notification-status` + + + +## [5.9.7] - 2021-08-04 +https://github.com/softlayer/softlayer-python/compare/v5.9.6...v5.9.7 + +#### 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. +- #1202 Order a virtual server private. + + +## [5.8.3] - 2019-12-11 +https://github.com/softlayer/softlayer-python/compare/v5.8.2...v5.8.3 + +- #771 Fixed unicode errors in image list (for windows) +- #1191 Fixed ordering virtual server dedicated from the CLI +- #1155 Fixed capacity restriction when ordering storage quotes +- #1192 Fixed hardware detail bandwidth allocation errors. + + +## [5.8.2] - 2019-11-15 +- https://github.com/softlayer/softlayer-python/compare/v5.8.1...v5.8.2 + + ++ #1186 Fixed a unit test that could fail if the test took too long to run. ++ #1183 Added a check to ensure subnet and vlan options are properly added to the order for virtual servers. ++ #1184 Fixed a readme misspelling. ++ #1182 Fixed vs reboot unable to resolve vs names. ++ #1095 Handle missing Fixtures better for unit tests. + +## [5.8.1] - 2019-10-11 +- https://github.com/softlayer/softlayer-python/compare/v5.8.0...v5.8.1 + ++ #1169 Drop python 2.7 support ++ #1170 Added CS# to ticket listing ++ #1162 Fixed issue looking up OS keyName instead of referenceCode ++ #627 Autoscale support + * slcli autoscale detail + * slcli autoscale edit + * slcli autoscale list + * slcli autoscale logs + * slcli autoscale scale + * slcli autoscale tag + +## [5.8.0] - 2019-09-04 +- https://github.com/softlayer/softlayer-python/compare/v5.7.2...v5.8.0 + ++ #1143 Upgrade to prompt_toolkit >= 2 ++ #1003 Bandwidth Feature + * slcli summary + * slcli report bandwidth + * slcli vs bandwidth + * slcli hw bandwidth + * Added bandwidth to VS and HW details page ++ #1146 DOCS: replace 'developer' with 'sldn' links ++ #1147 property 'contents' is not valid for 'SoftLayer_Ticket' when creating a ticket ++ #1139 cannot create static subnet with slcli ++ #1145 Refactor cdn network. ++ #1152 IBMID auth support ++ #1153, #1052 Transient VSI support ++ #1167 Removed legacy LoadBalancer command, added Citrix and IBM LBaaS commands. + * slcli lb cancel + * slcli lb detail + * slcli lb health + * slcli lb l7pool-add + * slcli lb l7pool-del + * slcli lb list + * slcli lb member-add + * slcli lb member-del + * slcli lb ns-detail + * slcli lb ns-list + * slcli lb order + * slcli lb order-options + * slcli lb pool-add + * slcli lb pool-del + * slcli lb pool-edit ++ #1157 Remove VpnAllowedFlag. ++ #1160 Improve hardware cancellation to deal with additional cases + +## [5.7.2] - 2019-05-03 +- https://github.com/softlayer/softlayer-python/compare/v5.7.1...v5.7.2 + ++ #1107 Added exception to handle json parsing error when ordering ++ #1068 Support for -1 when changing port speed ++ #1109 Fixed docs about placement groups ++ #1112 File storage endurance iops upgrade ++ #1101 Handle the new user creation exceptions ++ #1116 Fix order place quantity option ++ #1002 Invoice commands + * account invoices + * account invoice-detail + * account summary ++ #1004 Event Notification Management commands + * account events + * account event-detail ++ #1117 Two PCIe items can be added at order time ++ #1121 Fix object storage apiType for S3 and Swift. ++ #1100 Event Log performance improvements. ++ #872 column 'name' was renamed to 'hostname' ++ #1127 Fix object storage credentials. ++ #1129 Fixed unexpected errors in slcli subnet create ++ #1134 Change encrypt parameters for importing of images. Adds root-key-crn ++ #208 Quote ordering commands + * order quote + * order quote-detail + * order quote-list ++ #1113 VS usage information command + * virtual usage ++ #1131 made sure config_tests dont actually make api calls. + + +## [5.7.1] - 2019-02-26 +- https://github.com/softlayer/softlayer-python/compare/v5.7.0...v5.7.1 + ++ #1089 removed legacy SL message queue commands ++ Support for Hardware reflash firmware CLI/Manager method + +## [5.7.0] - 2019-02-15 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.6.4...v5.7.0 + ++ #1099 Support for security group Ids ++ event-log cli command ++ #1069 Virtual Placement Group Support + ``` + slcli vs placementgroup --help + Commands: + create Create a placement group. + create-options List options for creating a placement group. + delete Delete a placement group. + detail View details of a placement group. + list List placement groups. + ``` ++ #962 Rest Transport improvements. Properly handle HTTP exceptions instead of crashing. ++ #1090 removed power_state column option from "slcli server list" ++ #676 - ipv6 support for creating virtual guests + * Refactored virtual guest creation to use Product_Order::placeOrder instead of Virtual_Guest::createObject, because createObject doesn't allow adding IPv6 ++ #882 Added table which shows the status of each url in object storage ++ #1085 Update provisionedIops reading to handle float-y values ++ #1074 fixed issue with config setup ++ #1081 Fix file volume-cancel ++ #1059 Support for SoftLayer_Hardware_Server::toggleManagementInterface + * `slcli hw toggle-ipmi` + + +## [5.6.4] - 2018-11-16 + +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.6.3...v5.6.4 + ++ #1041 Dedicated host cancel, cancel-guests, list-guests ++ #1071 added createDate and modifyDate parameters to sg rule-list ++ #1060 Fixed slcli subnet list ++ #1056 Fixed documentation link in image manager ++ #1062 Added description to slcli order + +## [5.6.3] - 2018-11-07 + +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.6.0...v5.6.3 + ++ #1065 Updated urllib3 and requests libraries due to CVE-2018-18074 ++ #1070 Fixed an ordering bug ++ Updated release process and fab-file + +## [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://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` + * `slcli vs capacity detail` + * `slcli vs capacity list` ++ #1050 Fix `post_uri` parameter name on docstring ++ #1039 Fixed suspend cloud server order. ++ #1055 Update to use click 7 ++ #1053 Add export/import capabilities to/from IBM Cloud Object Storage to the image manager as well as the slcli. + + +## [5.5.3] - 2018-08-31 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.2...v5.5.3 + ++ Added `slcli user delete` ++ #1023 Added `slcli order quote` to let users create a quote from the slcli. ++ #1032 Fixed vs upgrades when using flavors. ++ #1034 Added pagination to ticket list commands ++ #1037 Fixed DNS manager to be more flexible and support more zone types. ++ #1044 Pinned Click library version at >=5 < 7 + +## [5.5.2] - 2018-08-31 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.1...v5.5.2 + ++ #1018 Fixed hardware credentials. ++ #1019 support for ticket priorities ++ #1025 create dedicated host with gpu fixed. + + +## [5.5.1] - 2018-08-06 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.0...v5.5.1 + +- #1006, added paginations to several slcli methods, making them work better with large result sets. +- #995, Fixed an issue displaying VLANs. +- #1011, Fixed an issue displaying some NAS passwords +- #1014, Ability to delete users + +## [5.5.0] - 2018-07-09 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.4.4...v5.5.0 + +- Added a warning when ordering legacy storage volumes +- Added documentation link to volume-order +- Increased slcli output width limit to 999 characters +- More unit tests +- Fixed an issue canceling some block storage volumes +- Fixed `slcli order` to work with network gateways +- Fixed an issue showing hardware credentials when they do not exist +- Fixed an issue showing addressSpace when listing virtual servers +- Updated ordering class to support baremetal servers with multiple GPU +- Updated prompt-toolkit as a fix for `slcli shell` +- Fixed `slcli vlan detail` to not fail when objects don't have a hostname +- Added user management + + +## [5.4.4] - 2018-04-18 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.4.3...v5.4.4 + +- fixed hw list not showing transactions +- Re-factored RestTransport and XMLRPCTransport, logging is now only done in the DebugTransport +- Added print_reproduceable to XMLRPCTransport and RestTransport, which should be very useful in printing out pure API calls. +- Fixed an issue with RestTransport and locationGroupId + + +## [5.4.3] - 2018-03-30 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.4.2...v5.4.3 + +- Corrected to current create-options output +- Allow ordering of account restricted presets +- Added lookup function for datacenter names and ability to use `slcli order` with short DC names +- Changed locatoinGroupId to check for None instead of empty string +- Added a way to try to cancel montly bare metal immediately. THis is done by automatically updating the cancellation request. A human still needs to read the ticket and process it for the reclaim to complete. + +## [5.4.2] - 2018-02-22 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.4.1...v5.4.2 + +- add GPU to the virtual create-options table +- Remove 'virtual' from the hardware ready command. +- Carefully check for the metric tracking id on virtual guests when building a bandwidth report. +- Do not fail if the source or destination subnet mask does not exist for ipv6 rules. + +## [5.4.1] - 2018-02-05 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.4.0...v5.4.1 + +- Improve error conditions when adding SSH keys +- added type filters to package-list, auto-removes bluemix_services on package listing +- Add boot mode option to virtual guest creation +- Update documentation for security group rule add +- Add fix for unsetting of values in edit SG rules + +## [5.4.0] - 2018-01-15 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.3.2...v5.4.0 + + - Upgraded Requests and Urllib3 library to latest. This allows the library to make use of connection retries, and connection pools. This should prevent the client from crashing if the API gives a connection reset / connection timeout error + - reworked wait_for_ready function for virtual, and added to hardware managers. + - fixed block/file iops in the `slcli block|file detail` view + - Added sub items to `hw detail --price`, removed reverse PTR entries + +### Added to CLI +- slcli order +``` +$ ./slcli order +Usage: slcli order [OPTIONS] COMMAND [ARGS]... + +Options: + -h, --help Show this message and exit. + +Commands: + category-list List the categories of a package. + item-list List package items used for ordering. + package-list List packages that can be ordered via the... + package-locations List Datacenters a package can be ordered in. + place Place or verify an order. + preset-list List package presets. +``` + + +## [5.3.2] - 2017-12-18 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.3.1...v5.3.2 + + - Expanded `@retry` useage to a few areas in the hardware manager + - Added INTERVAL options to block and file replication + - Fixed pricing error on `hw detail --price` + - Added sub items to `hw detail --price`, removed reverse PTR entries + +### Added to CLI +- slcli dedicatedhost + + +## [5.3.1] - 2017-12-07 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.3.0...v5.3.1 + - Added support for storage volume modifications + +### Added to CLI +- slcli block volume-modify +- slcli file volume-modify + +## [5.3.0] - 2017-12-01 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.15...v5.3.0 + - Added a retry decorator. currently only used in setTags for VSI creation, which should allos VSI creation to be a bit more robust. + - Updated unit tests to work with pytest3.3 ## [5.2.15] - 2017-10-30 - - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.14...master + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.14...v5.2.15 - Added dedicated host info to virt detail - #885 - Fixed createObjects on the rest api endpoint - changed securityGroups to use createObject instead of createObjects diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 460e436e7..45bce3aff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,12 +3,199 @@ 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 + +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 + +Code is tested and style checked with tox, you can run the tox tests individually by doing `tox -e ` + +* `autopep8 -r -v -i --max-line-length 119 SoftLayer/` +* `autopep8 -r -v -i --max-line-length 119 tests/` +* `tox -e analysis` +* `tox -e py36` +* `git commit --message="# ` +* `git push origin ` +* create pull request + + +## Documentation + +CLI command should have a more human readable style of documentation. +Manager methods should have a decent docblock describing any parameters and what the method does. + +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. + +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 + +All new features should be 100% code covered, and your pull request should at the very least increase total code overage. + +### Mocks +To tests results from the API, we keep mock results in SoftLayer/fixtures// with the method name matching the variable name. + +Any call to a service that doesn't have a fixture will result in a TransportError + +### Overriding Fixtures + +Adding your expected output in the fixtures file with a unique name is a good way to define a fixture that gets used frequently in a test. + +```python +from SoftLayer.fixtures import SoftLayer_Product_Package -* Fork the repo, make your changes, and open a pull request. + def test_test(self): + amock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + amock.return_value = fixtures.SoftLayer_Product_Package.RESERVED_CAPACITY +``` -* Additional infomration can be found in our [contribution guide](http://softlayer-python.readthedocs.org/en/latest/dev/index.html) +Otherwise defining it on the spot works too. +```python + def test_test(self): + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = { + 'billingItem': {'hourlyFlag': True, 'id': 449}, + } +``` + + +### Call testing +Testing your code to make sure it makes the correct API call is also very important. + +The testing.TestCase class has a method call `assert_called_with` which is pretty handy here. + +```python +self.assert_called_with( + 'SoftLayer_Billing_Item', # Service + 'cancelItem', # Method + args=(True, True, ''), # Args + identifier=449, # Id + mask=mock.ANY, # object Mask, + filter=mock.ANY, # object Filter + limit=0, # result Limit + offset=0 # result Offset +) +``` + +Making sure a API was NOT called + +```python +self.assertEqual([], self.calls('SoftLayer_Account', 'getObject')) +``` + +Making sure an API call has a specific arg, but you don't want to list out the entire API call (like with a place order test) + +```python +# Get the API Call signature +order_call = self.calls('SoftLayer_Product_Order', 'placeOrder') + +# Get the args property of that API call, which is a tuple, with the first entry being our data. +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 c0971279a..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 @@ -29,3 +30,4 @@ Swapnil Khanapurkar The SoftLayer Developer Network Tim Ariyeh Wissam Elriachy +Anthony Monthe (ZuluPro) diff --git a/README-internal.md b/README-internal.md new file mode 100644 index 000000000..5b25abda0 --- /dev/null +++ b/README-internal.md @@ -0,0 +1,78 @@ +This document is for internal users wanting to use this library to interact with the internal API. It will not work for `api.softlayer.com`. + +## SSL: CERTIFICATE_VERIFY_FAILED fix +You need to specify the server certificate to verify the connection to the internal API since its a self signed certificate. Python's request module doesn't use the system SSL cert for some reason, so even if you can use `curl` without SSL errors becuase you installed the certificate on your system, you still need to tell python about it. Further reading: + - https://hackernoon.com/solving-the-dreadful-certificate-issues-in-python-requests-module + - https://levelup.gitconnected.com/using-custom-ca-in-python-here-is-the-how-to-for-k8s-implementations-c450451b6019 + +On Mac, after installing the softlayer.local certificate, the following worked for me: + +```bash +security export -t certs -f pemseq -k /System/Library/Keychains/SystemRootCertificates.keychain -o bundleCA.pem +sudo cp bundleCA.pem /etc/ssl/certs/bundleCA.pem +``` +Alternatively +```bash +API_HOST= +echo quit | openssl s_client -showcerts -servername "${API_HOST}" -connect "${API_HOST}":443 > cacert.pem +``` + +Then in the `~/.softlayer` config, set `verify = /etc/ssl/certs/bundleCA.pem` and that should work. +You may also need to set `REQUESTS_CA_BUNDLE` -> `export REQUESTS_CA_BUNDLE=/etc/ssl/certs/bundleCA.pem` to force python to load your CA bundle + +## Certificate Example + +For use with a utility certificate. In your config file (usually `~/.softlayer`), you need to set the following: + +``` +[softlayer] +endpoint_url = https:///v3/internal/rest/ +timeout = 0 +theme = dark +auth_cert = /etc/ssl/certs/my_utility_cert-dev.pem +verify = /etc/ssl/certs/allCAbundle.pem +``` + +`auth_cert`: is your utility user certificate +`server_cert`: is the CA certificate bundle to validate the internal API ssl chain. Otherwise you get self-signed ssl errors without this. + + +```python +import SoftLayer +import logging +import click + +@click.command() +def testAuthentication(): + client = SoftLayer.CertificateClient() + result = client.call('SoftLayer_Account', 'getObject', id=12345, mask="mask[id,companyName]") + print(result) + + +if __name__ == "__main__": + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + testAuthentication() +``` + +## Employee Example + +To login with your employee username, have your config look something like this + +*NOTE*: Currently logging in with the rest endpoint doesn't quite work, so use xmlrpc until I fix [this issue](https://github.ibm.com/SoftLayer/internal-softlayer-cli/issues/10) + +``` +[softlayer] +username = +endpoint_url = https:///v3/internal/xmlrpc/ +verify = /etc/ssl/certs/allCAbundle.pem +``` + +You can login and use the `slcli` with. Use the `-i` flag to make internal API calls, otherwise it will make SLDN api calls. + +```bash +slcli -i emplogin +``` + +If you want to use any of the built in commands, you may need to use the `-a ` flag. diff --git a/README-snapcraft.md b/README-snapcraft.md new file mode 100644 index 000000000..12ec05bcc --- /dev/null +++ b/README-snapcraft.md @@ -0,0 +1,17 @@ +# To Install: + +`sudo snap install slcli` + +------------------------------------------------------------------------ + +# What are SNAPS? + +Snaps are available for any Linux OS running snapd, the service that runs and manage snaps. For more info, see: https://snapcraft.io/ + +or to learn to build and publish your own snaps, please see: +https://docs.snapcraft.io/build-snaps/languages?_ga=2.49470950.193172077.1519771181-1009549731.1511399964 + +# Releasing +Builds should be automagic here. + +https://build.snapcraft.io/user/softlayer/softlayer-python diff --git a/README.rst b/README.rst index b2c7617d8..5f82bdd62 100644 --- a/README.rst +++ b/README.rst @@ -1,37 +1,37 @@ 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://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 `_. +XML-RPC API `_. A command-line interface is also included and can be used to manage various SoftLayer products and services. -Development on this library is done as a best-effort delivery, and some features of the SoftLayer API may not be available through the client. 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: * `SoftLayer API reference - `_ + `_ * `Object mask information and examples - `_ + `_ +* `Code Examples + `_ Installation ------------ @@ -48,6 +48,20 @@ Or you can install from source. Download source and run: $ python setup.py install +Another (safer) method of installation is to use the published snap. Snaps are available for any Linux OS running snapd, the service that runs and manage snaps. Snaps are "auto-updating" packages and will not disrupt the current versions of libraries and software packages on your Linux-based system. To learn more, please visit: https://snapcraft.io/ + +To install the slcli snap: + +.. code-block:: bash + + $ sudo snap install slcli + + (or to get the latest release) + + $ sudo snap install slcli --edge + + + The most up-to-date version of this library can be found on the SoftLayer GitHub public repositories at http://github.com/softlayer. For questions regarding the use of this library please post to Stack Overflow at https://stackoverflow.com/ and your posts with “SoftLayer” so our team can easily find your post. To report a bug with this library please create an Issue on github. @@ -56,16 +70,121 @@ 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. + +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 `SLDN `_ + +Development +----------- +To get started working with this project please read the `CONTRIBUTING `_ document. + +You can quickly test local changes by running the './slcli' file, which will load the local softlayer-python code instead of the system's softlayer-python codebase. + +Debugging +--------- +To get the exact API call that this library makes, you can do the following. + +For the CLI, just use the -vvv option. If you are using the REST endpoint, this will print out a curl command that you can use, if using XML, this will print the minimal python code to make the request without the softlayer library. + +.. code-block:: bash + + $ slcli -vvv vs list + + +If you are using the library directly in python, you can do something like this. + +.. code-block:: python + + import SoftLayer + import logging + + class invoices(): + + def __init__(self): + self.client = SoftLayer.Client() + debugger = SoftLayer.DebugTransport(self.client.transport) + self.client.transport = debugger + + def main(self): + mask = "mask[id]" + account = self.client.call('Account', 'getObject', mask=mask); + print("AccountID: %s" % account['id']) + + def debug(self): + for call in self.client.transport.get_last_calls(): + print(self.client.transport.print_reproduceable(call)) + + if __name__ == "__main__": + main = example() + main.main() + main.debug() + + + System Requirements ------------------- -* Python 2.7, 3.3, 3.4, 3.5 or 3.6. +* 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 `_ . +If you cannot install python 3.6+ for some reason, you will need to use a version of softlayer-python <= 5.7.2 + + + +Python Packages +--------------- +* prettytable >= 2.5.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 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 75eea45dc..962ee1663 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,12 +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]" -* Push tag and PyPi `fab release:[VERSION_NUMBER]`. Before you do this, make sure you have the organization repository set up as upstream remote & fabric installed (`pip install fabric`), 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: + +# 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) +``` + +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 2a79264c2..cff277286 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -5,24 +5,36 @@ :license: MIT, see LICENSE for more details. """ -import warnings +# pylint: disable=invalid-name +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 -# pylint: disable=invalid-name - - +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(( @@ -34,7 +46,7 @@ 'raw_headers', 'limit', 'offset', - 'verify', + 'verify' )) @@ -81,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, @@ -128,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. - Deprecated in favor of create_client_from_env() + Settings are loaded via keyword arguments, environemtal variables and config file. + + :param username: your user ID + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token) + :param password: password to use for employee authentication + :param endpoint_url: the API endpoint base URL you wish to connect to. + Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. + :param proxy: proxy to be used to make API calls + :param integer timeout: timeout for API requests + :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. + Example: `BasicAuthentication` + :param config_file: A path to a configuration file used to load settings + :param user_agent: an optional User Agent to report when making API + calls if you wish to bypass the packages built in User Agent string + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) + :param bool verify: decide to verify the server's SSL/TLS cert. """ - warnings.warn("use SoftLayer.create_client_from_env() instead", - DeprecationWarning) + settings = config.get_client_settings(username=username, + api_key=None, + endpoint_url=endpoint_url, + timeout=timeout, + proxy=proxy, + verify=None, + config_file=config_file) + + url = settings.get('endpoint_url', '') + verify = settings.get('verify', True) + + if 'internal' not in url: + raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.") + + if transport is None: + if url is not None and '/rest' in url: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + else: + # Default the transport to use XMLRPC + transport = transports.XmlRpcTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + + if access_token is None: + access_token = settings.get('access_token') + + user_id = settings.get('userid') + # Assume access_token is valid for now, user has logged in before at least. + if settings.get('auth_cert', False): + auth = slauth.X509Authentication(settings.get('auth_cert'), verify) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + elif access_token and user_id: + auth = slauth.EmployeeAuthentication(user_id, access_token) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + else: + # This is for logging in mostly. + LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.") + return EmployeeClient(auth=None, transport=transport, config_file=config_file) + + +def Client(**kwargs): + """Get a SoftLayer API Client using environmental settings.""" return create_client_from_env(**kwargs) @@ -145,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 @@ -203,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 @@ -216,14 +339,16 @@ def call(self, service, method, *args, **kwargs): """ if kwargs.pop('iter', False): - return self.iter_call(service, method, *args, **kwargs) + # Most of the codebase assumes a non-generator will be returned, so casting to list + # keeps those sections working + return list(self.iter_call(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': '*/*'} @@ -246,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', {})) @@ -269,55 +387,86 @@ def iter_call(self, service, method, *args, **kwargs): :param service: the name of the SoftLayer API service :param method: the method to call on the service - :param integer chunk: result size for each API call (defaults to 100) + :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 + :param \\*\\*kwargs: same optional keyword arguments that ``Service.call`` takes """ - chunk = kwargs.pop('chunk', 100) - limit = kwargs.pop('limit', None) - offset = kwargs.pop('offset', 0) - if chunk <= 0: - raise AttributeError("Chunk size should be greater than zero.") + limit = kwargs.pop('limit', 100) + offset = kwargs.pop('offset', 0) - if limit: - chunk = min(chunk, limit) + if limit <= 0: + raise AttributeError("Limit size should be greater than zero.") - result_count = 0 + # Set to make unit tests, which call this function directly, play nice. kwargs['iter'] = False - while True: - if limit: - # We've reached the end of the results - if result_count >= limit: - break - - # Don't over-fetch past the given limit - if chunk + result_count > limit: - chunk = limit - result_count - - results = self.call(service, method, - offset=offset, limit=chunk, *args, **kwargs) + result_count = 0 + keep_looping = True + kwargs['filter'] = utils.fix_filter(kwargs.get('filter')) - # It looks like we ran out results - if not results: - break + while keep_looping: + # Get the next results + results = self.call(service, method, offset=offset, limit=limit, *args, **kwargs) # Apparently this method doesn't return a list. # Why are you even iterating over this? - if not isinstance(results, list): - yield results - break + if not isinstance(results, transports.SoftLayerListResult): + if isinstance(results, list): + # Close enough, this makes testing a lot easier + results = transports.SoftLayerListResult(results, len(results)) + else: + yield results + return for item in results: yield item result_count += 1 - offset += chunk + # Got less results than requested, we are at the end + if len(results) < limit: + keep_looping = False + # Got all the needed items + if result_count >= results.total_count: + keep_looping = False - if len(results) < chunk: - break + 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) @@ -328,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. @@ -335,6 +781,7 @@ class Service(object): :param name str: The service name """ + def __init__(self, client, name): self.client = client self.name = name diff --git a/SoftLayer/CLI/__init__.py b/SoftLayer/CLI/__init__.py index 5e4389c65..3d5d6bf79 100644 --- a/SoftLayer/CLI/__init__.py +++ b/SoftLayer/CLI/__init__.py @@ -5,6 +5,6 @@ :license: MIT, see LICENSE for more details. """ -# pylint: disable=w0401 +# pylint: disable=w0401, invalid-name from SoftLayer.CLI.helpers import * # NOQA diff --git a/SoftLayer/CLI/account/__init__.py b/SoftLayer/CLI/account/__init__.py new file mode 100644 index 000000000..50da7c7f0 --- /dev/null +++ b/SoftLayer/CLI/account/__init__.py @@ -0,0 +1 @@ +"""Account commands""" 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 new file mode 100644 index 000000000..38a29c1bb --- /dev/null +++ b/SoftLayer/CLI/account/event_detail.py @@ -0,0 +1,74 @@ +"""Details of a specific event, and ability to acknowledge event.""" +# :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') +@click.option('--ack', is_flag=True, default=False, + help="Acknowledge Event. Doing so will turn off the popup in the control portal") +@environment.pass_env +def cli(env, identifier, ack): + """Details of a specific event, and ability to acknowledge event.""" + + # Print a list of all on going maintenance + manager = AccountManager(env.client) + event = manager.get_event(identifier) + + if ack: + manager.ack_event(identifier) + + env.fout(basic_event_table(event)) + env.fout(impacted_table(event)) + env.fout(update_table(event)) + + +def basic_event_table(event): + """Formats a basic event table""" + table = formatting.Table(["Id", "Status", "Type", "Start", "End"], + title=utils.clean_splitlines(event.get('subject'))) + + table.add_row([ + event.get('id'), + utils.lookup(event, 'statusCode', 'name'), + utils.lookup(event, 'notificationOccurrenceEventType', 'keyName'), + utils.clean_time(event.get('startDate')), + utils.clean_time(event.get('endDate')) + ]) + + return table + + +def impacted_table(event): + """Formats a basic impacted resources table""" + table = formatting.Table([ + "Type", "Id", "Hostname", "PrivateIp", "Label" + ]) + for item in event.get('impactedResources', []): + table.add_row([ + item.get('resourceType'), + item.get('resourceTableId'), + item.get('hostname'), + item.get('privateIp'), + item.get('filterLabel') + ]) + return table + + +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') + 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 new file mode 100644 index 000000000..3dc59e329 --- /dev/null +++ b/SoftLayer/CLI/account/events.py @@ -0,0 +1,124 @@ +"""Summary and acknowledgement of upcoming and ongoing maintenance events""" +# :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('--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.") +@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, planned, unplanned, announcement, date_min): + """Summary and acknowledgement of upcoming and ongoing maintenance events""" + + if date_min: + utils.verify_date(date_min) + + manager = AccountManager(env.client) + planned_events = manager.get_upcoming_events("PLANNED", 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 + + +def planned_event_table(events): + """Formats a table for events""" + 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: + 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'), + ]) + 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 new file mode 100644 index 000000000..4436c44d9 --- /dev/null +++ b/SoftLayer/CLI/account/invoice_detail.py @@ -0,0 +1,106 @@ +"""Invoice details""" +# :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') +@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 + + 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", + "Monthly", "Create Date", "Location"], title=title) + table.align['category'] = 'l' + table.align['description'] = 'l' + for item in top_items: + fqdn = "%s.%s" % (item.get('hostName', ''), item.get('domainName', '')) + # category id=2046, ram_usage doesn't have a name... + category = utils.lookup(item, 'category', 'name') or item.get('categoryCode') + 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), + 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([ + '>>>', + utils.lookup(child, 'category', 'name'), + nice_string(child.get('description')), + "$%.2f" % float(child.get('oneTimeAfterTaxAmount')), + "$%.2f" % float(child.get('recurringAfterTaxAmount')), + '---', + '---' + ]) + return table + + +def sum_item_charges(item: dict) -> (float, float): + """Takes a billing Item, sums up its child items and returns recurring, one_time prices""" + + # API returns floats as strings in this case + single = float(item.get('oneTimeAfterTaxAmount', 0.0)) + recurring = float(item.get('recurringAfterTaxAmount', 0.0)) + for child in item.get('children', []): + single = single + float(child.get('oneTimeAfterTaxAmount', 0.0)) + recurring = recurring + float(child.get('recurringAfterTaxAmount', 0.0)) + + return (recurring, single) diff --git a/SoftLayer/CLI/account/invoices.py b/SoftLayer/CLI/account/invoices.py new file mode 100644 index 000000000..8f0281ec9 --- /dev/null +++ b/SoftLayer/CLI/account/invoices.py @@ -0,0 +1,47 @@ +"""Invoice listing""" +# :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('--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""" + + manager = AccountManager(env.client) + invoices = manager.get_invoices(limit, closed, get_all) + + table = formatting.Table([ + "Id", "Created", "Type", "Status", "Starting Balance", "Ending Balance", "Invoice Amount", "Items" + ]) + table.align['Starting Balance'] = 'l' + table.align['Ending Balance'] = 'l' + table.align['Invoice Amount'] = 'l' + table.align['Items'] = 'l' + if isinstance(invoices, dict): + invoices = [invoices] + for invoice in invoices: + table.add_row([ + invoice.get('id'), + utils.clean_time(invoice.get('createDate'), out_format="%Y-%m-%d"), + invoice.get('typeCode'), + invoice.get('statusCode'), + invoice.get('startingBalance'), + invoice.get('endingBalance'), + invoice.get('invoiceTotalAmount'), + invoice.get('itemCount') + ]) + env.fout(table) 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 new file mode 100644 index 000000000..2b4a7106d --- /dev/null +++ b/SoftLayer/CLI/account/summary.py @@ -0,0 +1,40 @@ +"""Account Summary page""" +# :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) +@environment.pass_env +def cli(env): + """Prints some various bits of information about an account""" + + manager = AccountManager(env.client) + summary = manager.get_summary() + env.fout(get_snapshot_table(summary)) + + +def get_snapshot_table(account): + """Generates a table for printing account summary data""" + table = formatting.KeyValueTable(["Name", "Value"], title="Account Snapshot") + table.align['Name'] = 'r' + table.align['Value'] = 'l' + table.add_row(['Company Name', account.get('companyName', '-')]) + table.add_row(['Balance', utils.lookup(account, 'pendingInvoice', 'startingBalance')]) + table.add_row(['Upcoming Invoice', utils.lookup(account, 'pendingInvoice', 'invoiceTotalAmount')]) + table.add_row(['Image Templates', account.get('blockDeviceTemplateGroupCount', '-')]) + table.add_row(['Dedicated Hosts', account.get('dedicatedHostCount', '-')]) + table.add_row(['Hardware', account.get('hardwareCount', '-')]) + table.add_row(['Virtual Guests', account.get('virtualGuestCount', '-')]) + table.add_row(['Domains', account.get('domainCount', '-')]) + table.add_row(['Network Storage Volumes', account.get('networkStorageCount', '-')]) + table.add_row(['Open Tickets', account.get('openTicketCount', '-')]) + table.add_row(['Network Vlans', account.get('networkVlanCount', '-')]) + table.add_row(['Subnets', account.get('subnetCount', '-')]) + table.add_row(['Users', account.get('userCount', '-')]) + return table 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 df76b60e6..4a5931e95 100644 --- a/SoftLayer/CLI/block/access/authorize.py +++ b/SoftLayer/CLI/block/access/authorize.py @@ -6,21 +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) @@ -30,16 +37,11 @@ def cli(env, volume_id, hardware_id, virtual_id, ip_address_id, ip_address): for ip_address_value in ip_address: ip_address_object = network_manager.ip_lookup(ip_address_value) if ip_address_object == "": - click.echo("IP Address not found on your account. " + - "Please confirm IP and try again.") + click.echo("IP Address not found on your account. Please confirm IP and try again.") raise exceptions.ArgumentError('Incorrect IP Address') - else: - ip_address_id_list.append(ip_address_object['id']) + ip_address_id_list.append(ip_address_object['id']) - block_manager.authorize_host_to_volume(volume_id, - hardware_id, - virtual_id, - ip_address_id_list) + block_manager.authorize_host_to_volume(volume_id, hardware_id, virtual_id, ip_address_id_list) # If no exception was raised, the command succeeded click.echo('The specified hosts were authorized to access %s' % volume_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 70a41c0c0..a4359fae3 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -5,109 +5,89 @@ 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('iops'): - table.add_row(['IOPs', block_volume['iops']]) + if block_volume.get('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']: - table.add_row([ - 'Ongoing Transactions', - trans['transactionStatus']['friendlyName']]) + 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['replicationPartnerCount']]) + 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'): - duplicate_info = formatting.Table(['Original Volume Name', - block_volume['originalVolumeName']]) - duplicate_info.add_row(['Original Volume Size', - block_volume['originalVolumeSize']]) - duplicate_info.add_row(['Original Snapshot Name', - block_volume['originalSnapshotName']]) - table.add_row(['Duplicate Volume Properties', duplicate_info]) + original_volume_info = formatting.Table(['Property', 'Value']) + original_volume_info.add_row(['Original Volume Size', block_volume['originalVolumeSize']]) + if block_volume.get('originalVolumeName'): + original_volume_info.add_row(['Original Volume Name', block_volume['originalVolumeName']]) + if block_volume.get('originalSnapshotName'): + 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 0ecf59110..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, @@ -22,9 +22,7 @@ 'the origin volume will be used.***\n' 'Potential Sizes: [20, 40, 80, 100, 250, ' '500, 1000, 2000, 4000, 8000, 12000] ' - 'Minimum: [the size of the origin volume] ' - 'Maximum: [the minimum of 12000 GB or ' - '10*(origin volume size)]') + 'Minimum: [the size of the origin volume]') @click.option('--duplicate-iops', '-i', type=int, help='Performance Storage IOPS, between 100 and 6000 in ' @@ -56,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 @@ -77,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 4cc9afd2b..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,11 +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', ('provisionedIops',), mask="provisionedIops"), column_helper.Column('ip_addr', ('serviceResourceBackendIpAddress',), mask="serviceResourceBackendIpAddress"), column_helper.Column('lunId', ('lunId',), mask="lunId"), @@ -33,6 +32,7 @@ column_helper.Column( 'created_by', ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), + column_helper.Column('notes', ('notes',), mask="notes"), ] DEFAULT_COLUMNS = [ @@ -41,40 +41,43 @@ 'datacenter', 'storage_type', 'capacity_gb', - 'bytes_used', + '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 new file mode 100644 index 000000000..187ff8d7e --- /dev/null +++ b/SoftLayer/CLI/block/modify.py @@ -0,0 +1,65 @@ +"""Modify 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 exceptions + + +CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()} + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, context_settings=CONTEXT_SETTINGS) +@click.argument('volume-id') +@click.option('--new-size', '-c', + type=int, + help='New Size of block volume in GB. ***If no size is given, the original size of volume is used.***\n' + 'Potential Sizes: [20, 40, 80, 100, 250, 500, 1000, 2000, 4000, 8000, 12000]\n' + 'Minimum: [the original size of the volume]') +@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.***') +@click.option('--new-tier', '-t', + 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. 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: + new_tier = float(new_tier) + + try: + order = block_manager.order_modified_volume( + volume_id, + new_size=new_size, + new_iops=new_iops, + new_tier_level=new_tier, + ) + except ValueError as ex: + raise exceptions.ArgumentError(str(ex)) + + if 'placedOrder' in order.keys(): + click.echo(f"Order #{order['placedOrder']['id']} placed successfully!") + for item in order['placedOrder']['items']: + click.echo(" > %s" % item['description']) + else: + click.echo("Order could not be placed! Please verify your options and try again.") 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 abd7dd91e..bdd0100e0 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -6,28 +6,24 @@ 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']), required=True) @click.option('--size', type=int, - help='Size of block storage volume in GB. Permitted Sizes:\n' - '20, 40, 80, 100, 250, 500, 1000, 2000, 4000, 8000, 12000', + help='Size of block storage volume in GB.', required=True) @click.option('--iops', type=int, - help='Performance Storage IOPs,' - ' between 100 and 6000 in multiples of 100' - ' [required for storage-type performance]') + help="""Performance Storage IOPs. Options vary based on storage size. +[required for storage-type performance]""") @click.option('--tier', - help='Endurance Storage Tier (IOP per GB)' - ' [required for storage-type endurance]', + help='Endurance Storage Tier (IOP per GB) [required for storage-type endurance]', type=click.Choice(['0.25', '2', '4', '10'])) @click.option('--os-type', help='Operating System', @@ -49,8 +45,8 @@ 'space along with endurance block storage; specifies ' 'the size (in GB) of snapshot space to order') @click.option('--service-offering', - help='The service offering package to use for placing ' - 'the order [optional, default is \'storage_as_a_service\']', + help="""The service offering package to use for placing the order. +[optional, default is \'storage_as_a_service\']. enterprise and performance are depreciated""", default='storage_as_a_service', type=click.Choice([ 'storage_as_a_service', @@ -63,7 +59,11 @@ @environment.pass_env def cli(env, storage_type, size, iops, tier, os_type, location, snapshot_size, service_offering, billing): - """Order a block storage volume.""" + """Order a block storage volume. + + Valid size and iops options can be found here: + https://cloud.ibm.com/docs/BlockStorage/index.html#provisioning-considerations + """ block_manager = SoftLayer.BlockStorageManager(env.client) storage_type = storage_type.lower() @@ -71,26 +71,22 @@ def cli(env, storage_type, size, iops, tier, os_type, if billing.lower() == "hourly": hourly_billing_flag = True - if hourly_billing_flag and service_offering != 'storage_as_a_service': - raise exceptions.CLIAbort( - 'Hourly billing is only available for the storage_as_a_service ' - 'service offering' - ) + if service_offering != 'storage_as_a_service': + 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') - - if iops % 100 != 0: - raise exceptions.CLIAbort( - 'Option --iops must be a multiple of 100' - ) + raise exceptions.CLIAbort('Option --iops required with Performance') if service_offering == 'performance' and snapshot_size is not None: raise exceptions.CLIAbort( - '--snapshot-size is not available for performance volumes ' - 'ordered with the \'performance\' service offering option' + '--snapshot-size is not available for performance service offerings. ' + 'Use --service-offering storage_as_a_service' ) try: @@ -110,8 +106,7 @@ def cli(env, storage_type, size, iops, tier, os_type, if storage_type == 'endurance': if tier is None: raise exceptions.CLIAbort( - 'Option --tier required with Endurance in IOPS/GB ' - '[0.25,2,4,10]' + 'Option --tier required with Endurance in IOPS/GB [0.25,2,4,10]' ) try: @@ -129,10 +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.") + 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 3887c29c9..e7f4dc737 100644 --- a/SoftLayer/CLI/block/replication/failback.py +++ b/SoftLayer/CLI/block/replication/failback.py @@ -6,18 +6,14 @@ 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") @environment.pass_env -def cli(env, volume_id, replicant_id): - """Failback a block volume from the given replicant volume.""" +def cli(env, volume_id): + """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, - replicant_id - ) + success = block_storage_manager.failback_from_replicant(volume_id) if success: click.echo("Failback from replicant is now in progress.") diff --git a/SoftLayer/CLI/block/replication/failover.py b/SoftLayer/CLI/block/replication/failover.py index 545175c4a..1bb63f84f 100644 --- a/SoftLayer/CLI/block/replication/failover.py +++ b/SoftLayer/CLI/block/replication/failover.py @@ -6,22 +6,17 @@ 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('--immediate', - is_flag=True, - default=False, - help="Failover to replicant immediately.") +@click.option('--replicant-id', help="ID of the replicant volume.") @environment.pass_env -def cli(env, volume_id, replicant_id, immediate): - """Failover a block volume to the given replicant volume.""" +def cli(env, volume_id, replicant_id): + """Failover a block volume to the given replica volume.""" block_storage_manager = SoftLayer.BlockStorageManager(env.client) success = block_storage_manager.failover_to_replicant( volume_id, - replicant_id, - immediate + replicant_id ) if success: 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 5aebea17e..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, ' - '(HOURLY | DAILY | WEEKLY)', - required=True, - type=click.Choice(['HOURLY', 'DAILY', 'WEEKLY'])) -@click.option('--location', '-l', - help='Short name of the data center for the replicant ' - '(e.g.: dal09)', +@click.option('--datacenter', '-d', + help='Short name of the datacenter for the replica (e.g.: dal09)', required=True) -@click.option('--tier', - help='Endurance Storage Tier (IOPS per GB) of the primary' - ' volume for which a replicant is ordered [optional]', - type=click.Choice(['0.25', '2', '4', '10'])) -@click.option('--os-type', - help='Operating System Type (e.g.: LINUX) of the primary' - ' volume for which a replica is ordered [optional]', +@click.option('--iops', '-i', + help='Performance Storage IOPs, between 100 and 6000 in multiples of 100. If no IOPS value is specified,' + ' the IOPS value of the original volume will be used.', + type=int) +@click.option('--os-type', '-o', + help='Operating System Type (eg. LINUX) of the primary volume for ' + 'which a replica is ordered [optional].', type=click.Choice([ 'HYPER_V', 'LINUX', @@ -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 f34d3483d..254b5bd1a 100644 --- a/SoftLayer/CLI/block/snapshot/disable.py +++ b/SoftLayer/CLI/block/snapshot/disable.py @@ -7,19 +7,18 @@ 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 [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 != 'HOURLY' and schedule_type != 'DAILY' - and schedule_type != 'WEEKLY'): + if (schedule_type not in ['INTERVAL', 'HOURLY', 'DAILY', 'WEEKLY']): raise exceptions.CLIAbort( - '--schedule-type must be HOURLY, DAILY, or WEEKLY') + '--schedule-type must be INTERVAL, HOURLY, DAILY, or WEEKLY') block_manager = SoftLayer.BlockStorageManager(env.client) disabled = block_manager.disable_snapshots(volume_id, schedule_type) diff --git a/SoftLayer/CLI/block/snapshot/enable.py b/SoftLayer/CLI/block/snapshot/enable.py index 6ade94647..ec54aa0c0 100644 --- a/SoftLayer/CLI/block/snapshot/enable.py +++ b/SoftLayer/CLI/block/snapshot/enable.py @@ -7,38 +7,41 @@ 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 [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 = {'HOURLY', 'DAILY', 'WEEKLY'} + valid_schedule_types = {'INTERVAL', 'HOURLY', 'DAILY', 'WEEKLY'} valid_days = {'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'} if schedule_type not in valid_schedule_types: raise exceptions.CLIAbort( - '--schedule-type must be HOURLY, DAILY, or WEEKLY, not ' - + schedule_type) + '--schedule-type must be INTERVAL, HOURLY, DAILY,' + + 'or WEEKLY, not ' + schedule_type) + if schedule_type == 'INTERVAL' and (minute < 30 or minute > 59): + raise exceptions.CLIAbort( + '--minute value must be between 30 and 59') if minute < 0 or minute > 59: raise exceptions.CLIAbort( '--minute value must be between 0 and 59') 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 new file mode 100644 index 000000000..a6c6ff2ec --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/schedule_list.py @@ -0,0 +1,70 @@ +"""List scheduled snapshots of a specific 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): + """Lists snapshot schedules for a given volume.""" + + block_manager = SoftLayer.BlockStorageManager(env.client) + + snapshot_schedules = block_manager.list_volume_schedules(volume_id) + + table = formatting.Table(['id', + 'active', + 'type', + 'replication', + 'date_created', + 'minute', + 'hour', + 'day', + 'week', + 'day_of_week', + 'date_of_month', + 'month_of_year', + 'maximum_snapshots']) + + for schedule in snapshot_schedules: + + if 'REPLICATION' in schedule['type']['keyname']: + replication = '*' + else: + replication = formatting.blank() + + block_schedule_type = schedule['type']['keyname'].replace('REPLICATION_', '') + block_schedule_type = block_schedule_type.replace('SNAPSHOT_', '') + + property_list = ['MINUTE', 'HOUR', 'DAY', 'WEEK', + 'DAY_OF_WEEK', 'DAY_OF_MONTH', + 'MONTH_OF_YEAR', 'SNAPSHOT_LIMIT'] + + schedule_properties = [] + for prop_key in property_list: + item = formatting.blank() + for schedule_property in schedule.get('properties', []): + if schedule_property['type']['keyname'] == prop_key: + if schedule_property['value'] == '-1': + item = '*' + else: + item = schedule_property['value'] + break + schedule_properties.append(item) + + table_row = [ + schedule['id'], + '*' if schedule.get('active', '') else '', + block_schedule_type, + replication, + schedule.get('createDate', '')] + table_row.extend(schedule_properties) + + table.add_row(table_row) + + env.fout(table) 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 0adb4fa31..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,51 +70,111 @@ 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. - \b - Examples: - slcli call-api Account getObject - slcli call-api Account getVirtualGuests --limit=10 --mask=id,hostname - slcli call-api Virtual_Guest getObject --id=12345 - slcli call-api Metric_Tracking_Object getBandwidthData --id=1234 \\ - "2015-01-01 00:00:00" "2015-01-1 12:00:00" public - slcli call-api Account getVirtualGuests \\ - -f 'virtualGuests.datacenter.name=dal05' \\ - -f 'virtualGuests.maxCpu=4' \\ - --mask=id,hostname,datacenter.name,maxCpu - slcli call-api Account getVirtualGuests \\ - -f 'virtualGuests.datacenter.name IN dal05,sng01' + For parameters that require a datatype, use a JSON string for that parameter. + Example:: + + slcli call-api Account getObject + slcli call-api Account getVirtualGuests --limit=10 --mask=id,hostname + slcli call-api Virtual_Guest getObject --id=12345 + slcli call-api Metric_Tracking_Object getBandwidthData --id=1234 \\ + "2015-01-01 00:00:00" "2015-01-1 12:00:00" public + slcli call-api Account getVirtualGuests \\ + -f 'virtualGuests.datacenter.name=dal05' \\ + -f 'virtualGuests.maxCpu=4' \\ + --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 509db5362..000000000 --- a/SoftLayer/CLI/cdn/detail.py +++ /dev/null @@ -1,32 +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('account_id') -@environment.pass_env -def cli(env, account_id): - """Detail a CDN Account.""" - - manager = SoftLayer.CDNManager(env.client) - account = manager.get_account(account_id) - - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - - table.add_row(['id', account['id']]) - table.add_row(['account_name', account['cdnAccountName']]) - table.add_row(['type', account['cdnSolutionName']]) - table.add_row(['status', account['status']['name']]) - table.add_row(['created', account['createDate']]) - table.add_row(['notes', - account.get('cdnAccountNote', formatting.blank())]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/list.py b/SoftLayer/CLI/cdn/list.py deleted file mode 100644 index 2e1b07785..000000000 --- a/SoftLayer/CLI/cdn/list.py +++ /dev/null @@ -1,43 +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(['id', - 'datacenter', - 'host', - 'cores', - 'memory', - 'primary_ip', - 'backend_ip'])) -@environment.pass_env -def cli(env, sortby): - """List all CDN accounts.""" - - manager = SoftLayer.CDNManager(env.client) - accounts = manager.list_accounts() - - table = formatting.Table(['id', - 'account_name', - 'type', - 'created', - 'notes']) - for account in accounts: - table.add_row([ - account['id'], - account['cdnAccountName'], - account['cdnSolutionName'], - account['createDate'], - account.get('cdnAccountNote', formatting.blank()) - ]) - - table.sortby = sortby - env.fout(table) diff --git a/SoftLayer/CLI/cdn/load.py b/SoftLayer/CLI/cdn/load.py deleted file mode 100644 index 648f4f34e..000000000 --- a/SoftLayer/CLI/cdn/load.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Cache one or more files on all edge nodes.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - - -@click.command() -@click.argument('account_id') -@click.argument('content_url', nargs=-1) -@environment.pass_env -def cli(env, account_id, content_url): - """Cache one or more files on all edge nodes.""" - - manager = SoftLayer.CDNManager(env.client) - manager.load_content(account_id, content_url) diff --git a/SoftLayer/CLI/cdn/origin_add.py b/SoftLayer/CLI/cdn/origin_add.py deleted file mode 100644 index 51d789da9..000000000 --- a/SoftLayer/CLI/cdn/origin_add.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Create an origin pull mapping.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - -# pylint: disable=redefined-builtin - - -@click.command() -@click.argument('account_id') -@click.argument('content_url') -@click.option('--type', - help='The media type for this mapping (http, flash, wm, ...)', - default='http', - show_default=True) -@click.option('--cname', - help='An optional CNAME to attach to the mapping') -@environment.pass_env -def cli(env, account_id, content_url, type, cname): - """Create an origin pull mapping.""" - - manager = SoftLayer.CDNManager(env.client) - manager.add_origin(account_id, type, content_url, cname) diff --git a/SoftLayer/CLI/cdn/origin_list.py b/SoftLayer/CLI/cdn/origin_list.py deleted file mode 100644 index 1867a9cdd..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('account_id') -@environment.pass_env -def cli(env, account_id): - """List origin pull mappings.""" - - manager = SoftLayer.CDNManager(env.client) - origins = manager.get_origins(account_id) - - table = formatting.Table(['id', 'media_type', 'cname', 'origin_url']) - - for origin in origins: - table.add_row([origin['id'], - origin['mediaType'], - origin.get('cname', formatting.blank()), - origin['originUrl']]) - - env.fout(table) diff --git a/SoftLayer/CLI/cdn/origin_remove.py b/SoftLayer/CLI/cdn/origin_remove.py deleted file mode 100644 index 2b8855ede..000000000 --- a/SoftLayer/CLI/cdn/origin_remove.py +++ /dev/null @@ -1,18 +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('account_id') -@click.argument('origin_id') -@environment.pass_env -def cli(env, account_id, origin_id): - """Remove an origin pull mapping.""" - - manager = SoftLayer.CDNManager(env.client) - manager.remove_origin(account_id, origin_id) diff --git a/SoftLayer/CLI/cdn/purge.py b/SoftLayer/CLI/cdn/purge.py deleted file mode 100644 index 7738600a3..000000000 --- a/SoftLayer/CLI/cdn/purge.py +++ /dev/null @@ -1,18 +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 - - -@click.command() -@click.argument('account_id') -@click.argument('content_url', nargs=-1) -@environment.pass_env -def cli(env, account_id, content_url): - """Purge cached files from all edge nodes.""" - - manager = SoftLayer.CDNManager(env.client) - manager.purge_content(account_id, content_url) diff --git a/SoftLayer/CLI/columns.py b/SoftLayer/CLI/columns.py index 18a9cea15..8cfdf0bd7 100644 --- a/SoftLayer/CLI/columns.py +++ b/SoftLayer/CLI/columns.py @@ -14,6 +14,7 @@ class Column(object): """Column desctribes an attribute and how to fetch/display it.""" + def __init__(self, name, path, mask=None): self.name = name self.path = path @@ -26,6 +27,7 @@ def __init__(self, name, path, mask=None): class ColumnFormatter(object): """Maps each column using a function""" + def __init__(self): self.columns = [] self.column_funcs = [] @@ -55,7 +57,7 @@ def mask(self): def get_formatter(columns): """This function returns a callback to use with click options. - The retuend function parses a comma-separated value and returns a new + The returned function parses a comma-separated value and returns a new ColumnFormatter. :param columns: a list of Column instances 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