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 411709398..5dd1975be 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ docs/_build/* build/* 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..f0aee0650 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,770 @@ +{ + "exclude": { + "files": "^.secrets.baseline$", + "lines": null + }, + "generated_at": "2025-06-11T21:28:32Z", + "plugins_used": [ + { + "name": "AWSKeyDetector" + }, + { + "name": "ArtifactoryDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "base64_limit": 4.5, + "name": "Base64HighEntropyString" + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "BoxDetector" + }, + { + "name": "CloudantDetector" + }, + { + "ghe_instance": "github.ibm.com", + "name": "GheDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "hex_limit": 3, + "name": "HexHighEntropyString" + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "keyword_exclude": null, + "name": "KeywordDetector" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "results": { + "RELEASE.md": [ + { + "hashed_secret": "564e340cd48437d2dfe876ee154cc99dc4d0d137", + "is_secret": false, + "is_verified": false, + "line_number": 67, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/CLI/dns/record_add.py": [ + { + "hashed_secret": "826feff6caff89ca2f2408dce0f2d9caecf9dc5f", + "is_secret": false, + "is_verified": false, + "line_number": 63, + "type": "Base64 High Entropy String", + "verified_result": null + } + ], + "SoftLayer/CLI/user/list.py": [ + { + "hashed_secret": "71206af1d24cf9ddf0c9a804ef700ed7fb3cb5ce", + "is_secret": false, + "is_verified": false, + "line_number": 11, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Account.py": [ + { + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", + "is_secret": false, + "is_verified": false, + "line_number": 122, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "df51e37c269aa94d38f93e537bf6e2020b21406c", + "is_secret": false, + "is_verified": false, + "line_number": 1036, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Event_Log.py": [ + { + "hashed_secret": "2c0ceacd445f15ebc02315e18fb3ed8ec73a61a0", + "is_secret": false, + "is_verified": false, + "line_number": 25, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "f08bf4f915242a2700e861e4e073ab45dc745e92", + "is_secret": false, + "is_verified": false, + "line_number": 32, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc", + "is_secret": false, + "is_verified": false, + "line_number": 47, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "6e61399506056ac598fc283b3be0aecf80a51952", + "is_secret": false, + "is_verified": false, + "line_number": 61, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "806f21b4bc195ffd5749f295b83909d66a56ff38", + "is_secret": false, + "is_verified": false, + "line_number": 79, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "1c89f7ca3440fe5db16e3b0ffe414d11845331d9", + "is_secret": false, + "is_verified": false, + "line_number": 85, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "5eb37c21d01d15fab7b546ee8fd1b50080fef2a3", + "is_secret": false, + "is_verified": false, + "line_number": 96, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "99e9638f573f92843c387930bec48bc75c854b90", + "is_secret": false, + "is_verified": false, + "line_number": 103, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "ee85b0f2b6ab5557b3b240d3a454e449ab651ee2", + "is_secret": false, + "is_verified": false, + "line_number": 114, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Hardware.py": [ + { + "hashed_secret": "49901d945ad6da0f0af47691f305daf994d9d2c9", + "is_secret": false, + "is_verified": false, + "line_number": 43, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "e1f0942738bb56a9905ac28a05c381ba1ca0a4e2", + "is_secret": false, + "is_verified": false, + "line_number": 47, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "7288edd0fc3ffcbe93a0cf06e3568e28521687bc", + "is_secret": false, + "is_verified": false, + "line_number": 122, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Hardware_Server.py": [ + { + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", + "is_secret": false, + "is_verified": false, + "line_number": 54, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 274, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Application_Delivery_Controller.py": [ + { + "hashed_secret": "df51e37c269aa94d38f93e537bf6e2020b21406c", + "is_secret": false, + "is_verified": false, + "line_number": 34, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Message_Delivery_Email_Sendgrid.py": [ + { + "hashed_secret": "707296a56c05e7213079ef340c13c2f383471b92", + "is_secret": false, + "is_verified": false, + "line_number": 33, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Storage_Hub_Cleversafe_Account.py": [ + { + "hashed_secret": "87e3789cb5540dfb78446e7beec33649dc8940c5", + "is_secret": false, + "is_verified": false, + "line_number": 31, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "f80a4da46034866bb3b6eee29b45d084c5f7829b", + "is_secret": false, + "is_verified": false, + "line_number": 31, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "c8d74351f47fcc09d44ccf063ca535f1056ff5cf", + "is_secret": false, + "is_verified": false, + "line_number": 74, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "f055a8f21c40658e6a2bf2c2c112fce3fc059148", + "is_secret": false, + "is_verified": false, + "line_number": 74, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "6ef8455158f5f522d25a813c6a7082fab8f7d7cd", + "is_secret": false, + "is_verified": false, + "line_number": 84, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "c26cca6e5b560bb54203b63596e243baa3e0afcc", + "is_secret": false, + "is_verified": false, + "line_number": 84, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "1685a5baa79a864b222e6a013d285cc553dd2de8", + "is_secret": false, + "is_verified": false, + "line_number": 92, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Storage_Iscsi.py": [ + { + "hashed_secret": "ed4ad870c35e2c96f8b59bc6c12b0f1262175e38", + "is_secret": false, + "is_verified": false, + "line_number": 17, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Network_Vlan_Firewall.py": [ + { + "hashed_secret": "9bc34549d565d9505b287de0cd20ac77be1d3f2c", + "is_secret": false, + "is_verified": false, + "line_number": 57, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Security_Certificate.py": [ + { + "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", + "is_secret": false, + "is_verified": false, + "line_number": 14, + "type": "Private Key", + "verified_result": null + } + ], + "SoftLayer/fixtures/SoftLayer_Virtual_Guest.py": [ + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 936, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/fixtures/full.conf": [ + { + "hashed_secret": "d332bc701dd6999e9de0ea46f3127031250634d3", + "is_secret": false, + "is_verified": false, + "line_number": 3, + "type": "Secret Keyword", + "verified_result": null + } + ], + "SoftLayer/managers/vs_capacity.py": [ + { + "hashed_secret": "8af1f8146d96a3cd862281442d0d6c5cb6f8f9e5", + "is_secret": false, + "is_verified": false, + "line_number": 133, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "SoftLayer/transports/soap.py.unstable": [ + { + "hashed_secret": "813c25388cd13e54d03723a57f678007399997e2", + "is_secret": false, + "is_verified": false, + "line_number": 59, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "eb985bdb5ffec689d0019ea0a9443bea2105738a", + "is_secret": false, + "is_verified": false, + "line_number": 89, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/api/client.rst": [ + { + "hashed_secret": "89a6cfe2a229151e8055abee107d45ed087bbb4f", + "is_secret": false, + "is_verified": false, + "line_number": 50, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/block.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 18, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/hardware.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 36, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/nas.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 10, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/users.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 75, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/cli/vs.rst": [ + { + "hashed_secret": "2bd8e9c9c868efe968cc583d2d49f67380967d94", + "is_secret": false, + "is_verified": false, + "line_number": 262, + "type": "Secret Keyword", + "verified_result": null + } + ], + "docs/config_file.rst": [ + { + "hashed_secret": "0f2f17651724aa4ec1676466b1e530992495a124", + "is_secret": false, + "is_verified": false, + "line_number": 25, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "798eabe0e81cecca592e52d37f2425494207a80f", + "is_secret": false, + "is_verified": false, + "line_number": 35, + "type": "Secret Keyword", + "verified_result": null + } + ], + "output.txt": [ + { + "hashed_secret": "81448fe273247b533b9f018e96c158cab7901247", + "is_secret": false, + "is_verified": false, + "line_number": 726, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "59b189070af751d4e93a749ccffb4ccfd2de7ab5", + "is_secret": false, + "is_verified": false, + "line_number": 1337, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "bbccdf2efb33b52e6c9d0a14dd70b2d415fbea6e", + "is_secret": false, + "is_verified": false, + "line_number": 1776, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "fe77c82bb2a42efeec9303600c8e7f6df56b6faf", + "is_secret": false, + "is_verified": false, + "line_number": 1923, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "4dc73741b9473168444fab7e680b439ba69f41ec", + "is_secret": false, + "is_verified": false, + "line_number": 3101, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/CLI/modules/firewall_tests.py": [ + { + "hashed_secret": "9bc34549d565d9505b287de0cd20ac77be1d3f2c", + "is_secret": false, + "is_verified": false, + "line_number": 90, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/CLI/modules/hardware/hardware_basic_tests.py": [ + { + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", + "is_secret": false, + "is_verified": false, + "line_number": 57, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/CLI/modules/securitygroup_tests.py": [ + { + "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc", + "is_secret": false, + "is_verified": false, + "line_number": 339, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "tests/api_tests.py": [ + { + "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", + "is_secret": false, + "is_verified": false, + "line_number": 81, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/auth_tests.py": [ + { + "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", + "is_secret": false, + "is_verified": false, + "line_number": 33, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/functional_tests.py": [ + { + "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", + "is_secret": false, + "is_verified": false, + "line_number": 31, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/block_tests.py": [ + { + "hashed_secret": "f7a9e24777ec23212c54d7a350bc5bea5477fdbb", + "is_secret": false, + "is_verified": false, + "line_number": 1077, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/hardware_tests.py": [ + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 673, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/image_tests.py": [ + { + "hashed_secret": "8de91b1f4c8ca32302ae101da16fb88fb127582a", + "is_secret": false, + "is_verified": false, + "line_number": 168, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "2da422d13be8072a8dcae1e46b36add9cb2372fa", + "is_secret": false, + "is_verified": false, + "line_number": 193, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/ipsec_tests.py": [ + { + "hashed_secret": "b310da45b1ebf444106a41b7832ab2fbe25dab41", + "is_secret": false, + "is_verified": false, + "line_number": 275, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "5b399c20d855de2450baab07ed09726b326cfeb1", + "is_secret": false, + "is_verified": false, + "line_number": 279, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/network_tests.py": [ + { + "hashed_secret": "2c0ceacd445f15ebc02315e18fb3ed8ec73a61a0", + "is_secret": false, + "is_verified": false, + "line_number": 545, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "f08bf4f915242a2700e861e4e073ab45dc745e92", + "is_secret": false, + "is_verified": false, + "line_number": 552, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "806f21b4bc195ffd5749f295b83909d66a56ff38", + "is_secret": false, + "is_verified": false, + "line_number": 584, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "1c89f7ca3440fe5db16e3b0ffe414d11845331d9", + "is_secret": false, + "is_verified": false, + "line_number": 590, + "type": "Hex High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc", + "is_secret": false, + "is_verified": false, + "line_number": 597, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "tests/managers/object_storage_tests.py": [ + { + "hashed_secret": "2551e332f3a8c04696365d595601ddf806f4b799", + "is_secret": false, + "is_verified": false, + "line_number": 81, + "type": "Base64 High Entropy String", + "verified_result": null + }, + { + "hashed_secret": "490a5c1209ddffbb772dfd6d9e8873f295362bcf", + "is_secret": false, + "is_verified": false, + "line_number": 81, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/managers/vs/vs_capacity_tests.py": [ + { + "hashed_secret": "8af1f8146d96a3cd862281442d0d6c5cb6f8f9e5", + "is_secret": false, + "is_verified": false, + "line_number": 187, + "type": "Hex High Entropy String", + "verified_result": null + } + ], + "tests/managers/vs/vs_tests.py": [ + { + "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e", + "is_secret": false, + "is_verified": false, + "line_number": 1149, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/transports/rest_tests.py": [ + { + "hashed_secret": "9878e362285eb314cfdbaa8ee8c300c285856810", + "is_secret": false, + "is_verified": false, + "line_number": 313, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/transports/soap_tests.py.unstable": [ + { + "hashed_secret": "8bb6118f8fd6935ad0876a3be34a717d32708ffd", + "is_secret": false, + "is_verified": false, + "line_number": 42, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "f0e99a0fcd86f5764d44e0518947046a29ca7245", + "is_secret": false, + "is_verified": false, + "line_number": 117, + "type": "Secret Keyword", + "verified_result": null + } + ], + "tests/transports/xmlrpc_tests.py": [ + { + "hashed_secret": "f08c5dc4980df3c1237e88b872a2429dac6be328", + "is_secret": false, + "is_verified": false, + "line_number": 297, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "7e6a3680012346b94b54731e13d8a9ffa3790645", + "is_secret": false, + "is_verified": false, + "line_number": 383, + "type": "Secret Keyword", + "verified_result": null + } + ] + }, + "version": "0.13.1+ibm.62.dss", + "word_list": { + "file": null, + "hash": null + } +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9abe38143..000000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +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: "pypy" - env: TOX_ENV=pypy - - python: "2.7" - env: TOX_ENV=analysis - - python: "2.7" - env: TOX_ENV=coverage - allow_failures: - - python: "nightly" - env: TOX_ENV=py35 -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 b/CHANGELOG deleted file mode 100644 index 1d4f172ab..000000000 --- a/CHANGELOG +++ /dev/null @@ -1,251 +0,0 @@ -4.1.1 - - * Fixes to work with Click v5 - - * Re-adds `--no-public` option to only provision private interfaces with servers via `slcli server create` - - * Removes non-functional `--vlan-public` and `--vlan-private` from `slcli server create` - - * VSManager.wait_for_ready will now behave as it is documented to behave. - -4.1.0 - - * Adds a shell which provides a shell interface for `slcli`. This is available by using `slcli shell` - - * `slcli vs create` and `slcli server create` will now prompt for missing required options - - * Fixes `slcli firewall add` command - - * Handles case where `slcli vs detail` and `slcli server detail` was causing an error when trying to display the creator - - * Fixes VSManager.verify_create_instance() with tags (and, in turn, `slcli vs create --test` with tags) - - * Fixes `vs resume` command - - * Updates hardware ordering to deal with location-specific prices - - * Fixes several description errors in the CLI - - * Running `vs edit` without a tag option will no longer remove all tags - - * Adds editing of hardware tags - -4.0.4 - - * Fixes bug with pulling the userData property for the virtual server detail - - * Fixes a class of bugs invloving unicode from the API - -4.0.3 - - * Fixes bug with `slcli vs ready` command - - * Fixes bug with `slcli loadbal service-add` command - - * Fixes bug with `slcli vlan list` with vlans that don't have a datacenter - - * Improves validation of virtual server and hardware create commands - -4.0.2 - - * Fixes a bug that breaks user confirmation prompts - - * Fixes general issue with sorting on certain row types in the CLI - - * Fixes image capture for Windows guests - - -4.0.1 - - * Fixes bug in `sl setup` command not properly defaulting to current values. - - * Fixes bug where turning off compression headers would still send compression headers. - - * Reverts to using ids over global identifiers for `sl vs list` and `sl server list`. - - -4.0.0 - - * Because there are many changes between version 3 and version 4, it is strongly recommend to pin the version of the SoftLayer python bindings as soon as you can in order to prevent unintentional breakage when upgrading. To keep yourself on version 3, you can use this directive: softlayer>=3,<4. That can be used with pip (pip install softlayer>=3,<4), requirements in your setup.py and/or in your requirements.txt file. - - * CLI: The command is renamed from `sl` to `slcli` to avoid package conflicts. - - * CLI: Global options now need to be specified right after the `slcli` command. For example, you would now use `slcli --format=raw list` over `slcli vs list --format=raw`. This is a change for the following options: - * --format - * -c or --config - * --debug - * --proxy - * -y or --really - * --version - - * API: The hardware manager has a significant update to how place_order() works. It will now only support the fast server provisioning package which has presets for options like CPU, Memory and disk. - - * API: The client transport is now pluggable. If you want to add extra logging or accounting, you can now subclass or wrap softlayer.transports.XmlRpcTransport in order to do so. A good example of that is done with softlayer.transports.TimingTransport. - - * API: Removed deprecated SoftLayer.CCIManager. - - * API: Adds virtual server rescue command to SoftLayer.VSManager - - * API+CLI: Adds ability to import virtual images from a given URI. The API only supports importing from a swift account using 'swift://'. For more details, see http://developer.softlayer.com/reference/services/SoftLayer_Virtual_Guest_Block_Device_Template_Group/createFromExternalSource. - - * CLI: A `--fixtures` global flag was added to pull from fixture data instead of the API. This is useful for discovery, demonstration and testing purposes. - - * CLI: A `--verbose` or `-v` flag was added to eventually replace `--debug`. To make a command more verbose, simply add more `-v` flags. For example `sl -vvv vs list` will be the most verbose and show everything down to request/response tracing. - - * CLI: Significant changes were done to the CLI argument parsing. Docopt was dropped in favor of click. Therefore, some subtle differences which aren't documented here may exist. - - * CLI: Credentials can now be requested using `sl vs credentials `, `sl hardware credentials ` and `sl nas credentials ` for virtual servers, hardware servers and NAS accounts respectively. - - * CLI: Adds virtual server rescue command, `sl vs rescue ` - - -3.3.0 - - * CLI+API: Load balancer support - - * CLI: More detail added to the `sl image detail` and `sl image list` commands - - * CLI: Adds command to import DNS entries from BIND zone files - - * CLI+API: Adds support for booting into rescue images for virtual servers and hardware - - * API: Adds ability to order virtual and hardare servers from a quote to the ordering manager - - * CLI: Fixes bug with `sl server list-chassis` and `sl server list-chassis` - - * API: Restructure of the way custom authentication can be plugged in the API client - - * Several other bug fixes - -3.2.0 - - * CLI+API: Added firewall manager and CLI module - - * CLI+API: Added iscsi manager and CLI module - - * API: Added ability to create multiple virtual servers at once to VSManager - - * API: Added OrderingManager. Remove hard-coded price IDs - - * Fixed several small bugs - -3.1.0 - - * CLI+API: Added CDN manager and CLI module - - * CLI+API: Added ticket manager and CLI module - - * CLI+API: Added image manager and improves image CLI module - - * CLI+API: Added the ability to specify a proxy URL for API bindings and the CLI - - * API: six is now used to provide support for Python 2 and Python 3 with the same source - - * CLI+API: Added ability to resize a virtual machine - - * CLI+API: Implemented product name changes in accordance with SoftLayer's new product names. Existing managers should continue to work as before. Minor CLI changes were necessary. - - * CLI+API: Added firewall manager and CLI module - - * CLI+API: Added load balancer manager and CLI module - - * Many bug fixes and minor suggested improvements - - -3.0.2 - - * CLI+API: Simplified object mask reformatting and added support for more complex masks. - - * CLI: Fixed the sl bmc create --network argument. - - * CLI+API: Improved output of the message queue feature and fixed some minor bugs. - - * CLI: Fixed an error when using --test and ordering a non-private subnet. - - * API: Fix to prevent double counting results in summary_by_datacenter(). - - * CLI+API: Added IPMI IP address to hardware details. - - * CLI: Added support for ordering multiple disks when creating a CCI. - - * API: Added flag to disable compression on HTTP requests. - - * CLI: Added CIDR information to subnet displays. - -3.0.1 - - * CLI: Fixed an error message about pricing information that appeared when ordering a new private subnet. - - * CLI+API: Added ability to specify SSH keys when reloading CCIs and servers. - -3.0.0 - - * Many bug fixes and consistency improvements - - * API: Removes old API client interfaces which have been deprecated in the v2. See link for more details: https://softlayer-api-python-client.readthedocs.org/en/latest/api/client/#backwards-compatibility - - * CLI+API: Improved dedicated server ordering. Adds power management for hardware servers: power-on, power-off, power-cycle, reboot - - * CLI+API: Adds a networking manager and adds several network-related CLI modules. This includes the ability to: - - * list, create, cancel and assign global IPs - - * list, create, cancel and detail subnets. Also has the ability to lookup details about an IP address with 'sl subnet lookup' - - * list, detail VLANs - - * show and edit RWhois data - - * CLI+API: Adds SoftLayer Message Queue Service bindings (as a manager) and a CLI counterpart. With this you can interact with existing message queue accounts - - * CLI+API: Ability to manage SSH Keys with a manager and a CLI module - - * CLI+API: Adds the ability to create CCIs with the following options: metadata, post-install script, SSH key - - * CLI: Adds templating for creating CCIs and hardware nodes which can be used to create more CCIs and hardware with the same settings - - * CLI+API: Adds the ability to create hardware servers with a default SSH key - - * CLI: Adds a --debug option to print out debugging information. --debug=3 is the highest log level which prints full HTTP request/responses including the body - - * CLI: The commands in the main help are now organized into categories - - -2.3.0 - - * Several bug fixes and improvements - - * Removed Python 2.5 support. Some stuff MIGHT work with 2.5 but it is no longer tested - - * API: Refactored managers into their own module to not clutter the top level - - * CLI+API: Added much more hardware support: Filters for hardware listing, dedicated server/bare metal cloud ordering, hardware cancellation - - * CLI+API: Added DNS Zone filtering (server side) - - * CLI+API: Added Post Install script support for CCIs and hardware - - * CLI: Added Message queue functionality - - * CLI: Added --debug option to CLI commands - - * API: Added more logging - - * API: Added token-based auth so you can use the API bindings with your username/password if you want. (It's still highly recommended to use your API key instead of your password) - - -2.2.0 - - * Consistency changes/bug fixes - - * Added sphinx documentation. See it here: https://softlayer-api-python-client.readthedocs.org - - * CCI: Adds Support for Additional Disks - - * CCI: Adds a way to block until transactions are done on a CCI - - * CLI: For most CCI commands, you can specify id, hostname, private ip or public ip as - - * CLI: Adds the ability to filter list results for CCIs - - * API: for large result sets, requests can now be chunked into smaller batches on the server side. Using service.iter_call('getObjects', ...) or service.getObjects(..., iter=True) will return a generator regardless of the results returned. offset and limit can be passed in like normal. An additional named parameter of 'chunk' is used to limit the number of items coming back in a single request, defaults to 100 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..abee1bea0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1152 @@ +# 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...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 + - Always set the endpoint_url by defaulting to the public URL if the endpoint type cannot be determined. + - resource metadata update + +## [5.2.14] - 2017-09-13 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.13...v5.2.14 + - Improved slcli vs create-options output + - Updated slcli vs create to support new virtual server public and dedicated host offerings + +## [5.2.13] - 2017-09-05 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.12...v5.2.13 + - Support for hourly billing of storage + - Added exception handling for Managers.VSManager.wait_for_ready() + - Added windows support for unit testing + - Updated pypy version + +## [5.2.12] - 2017-08-09 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.11...v5.2.12 + - Support for storage_as_a_service block and file storage + +#### Added to CLI + - block volume-count + - file volume-count + - securitygroups + - create Create a security group. + - delete Deletes the given security group + - detail Get details about a security group. + - edit Edit details of a security group. + - interface-add Attach an interface to a security group. + - interface-list List interfaces associated with security... + - interface-remove Detach an interface from a security group. + - list List security groups. + - rule-add Add a security group rule to a security... + - rule-edit Edit a security group rule in a security... + - rule-list List security group rules. + - rule-remove + +## [5.2.11] - 2017-08-04 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.10...v5.2.11 + - Sync VLAN and subnet detail CLI output + +## [5.2.10] - 2017-07-27 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.9...v5.2.10 + - Avoid blindly passing memory result to formatter + +## [5.2.9] - 2017-07-27 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.8...v5.2.9 + - Add support for dedicated host instances to virtual server upgrades +#### Added to CLI +* block volume-set-lun-id + +## [5.2.8] - 2017-07-19 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.7...v5.2.8 + + * Resolved https://github.com/softlayer/softlayer-python/issues/835 + * Resolved https://github.com/softlayer/softlayer-python/issues/826 + * Fix dedicated/private VSI price retrieval for upgrades + +#### Added to CLI +* block access-password + +## [5.2.7] - 2017-06-22 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.6...v5.2.7 + +Adds support for duplicating block and file storage volumes. Only works on Storage as a Service volumes (Volumes that support encryption at rest). + +#### Added to CLI + * [block|file] volume-duplicate + +## [5.2.6] - 2017-05-22 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.5...v5.2.6 + +#### Added To CLI +* ipsec list +* ipsec detail +* ipsec configure +* ipsec update +* ipsec subnet-add +* ipsec subnet-remove +* ipsec translation-add +* ipsec translation-remove +* ipsec translation-update + + +## [5.2.5] - 2017-05-05 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.1...v5.2.5 + +The SoftLayer_Network_Storage::storageTierLevel relational property changed in https://softlayer.github.io/release_notes/20170503/ , this version fixes problems caused by that. + +### Changed + - https://github.com/softlayer/softlayer-python/issues/818 + - https://github.com/softlayer/softlayer-python/pull/817 + +## [5.2.4] - 2017-04-06 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.3...v5.2.4 + +### Changed +Removed some debug code that was accidently added in the pypi release + +## [5.2.3] - 2017-04-05 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.2...v5.2.3 + +### Added + - Adds Python 3.6 support + +### Changed + - CLI+API: Removes the iSCSI manager and commands + - API: Fixes hardware order failing to find a single bare metal fast provision package to use + +## [5.2.2] - 2017-02-24 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.1...v5.2.2 + +### Added + - Adds release process documentation + - CLI: Displays NFS mount point for volumes in volume list and detail commands + - CLI+API: Enables `slcli file` and `block` storage commands to order tier 10 endurance storage and replica + +### Changed + - Updates docs to replace `sl` command with `slcli` + - CLI: Removes requirement to have `--os-type` provided for file storage ordering + - API: Fixes block storage ordering to handle size provided properly + - CLI: Fixes load balancer detail output so that JSON output is sane + - API: Includes check if object storage endpoints were provided by the API before trying to add them to the endpoints returned by `list_endpoints` + +## [5.2.1] - 2016-10-4 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.2.0...v5.2.1 + +### Added + - CLI: Adds a new 'jsonraw' format option that will print JSON without whitespace. This is useful for integrating the CLI with other tooling. + +### Changed + - API: Fixes JSON loading while using the REST transport with Python 3 + - CLI+API: Metadata disks are now excluded when capturing "all" block devices with `slcli virtual capture --all` + - CLI: Fixes a bug where dns zone importing was not importing wildcard records + +## [5.2.0] - 2016-08-25 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.1.0...v5.2.0 + +### Added + - CLI+API: Significant additions to `slcli file` and `slcli block` commands. You can now authorize hosts, revoke access. You can also create, delete, restore, disable, enable snapshots. These features need to be battle-tested so report any issues that you see. + - CLI+API: Adds logic to `SoftLayer.create_client_from_env` that detects if a REST endpoint_url was given in order to use the REST transport automatically. This means that you can also configure REST endpoints for `slcli`. The default still uses XML-RPC endpoint, but from a small amount of testing shows that the REST transport is significantly faster. + - CLI: Adds `--network-space` to `slcli subnet list` in order to filter subnets based on network space. The two main options are PUBLIC and PRIVATE. For example, to list all public subnets, you can run: `slcli subnet list --network-space=PUBLIC` + - CLI: Adds a new, non-default column, "created_by" that shows who ordered the volume for `slcli file volume-list` and `slcli block volume-list`. + - CLI: Adds a new `slcli report bandwidth` command that will print a report of all bandwidth pools and virtual/hardware servers that your user has access to. + - CLI: Adds an "IN" syntax to the `slcli call-api` command. For example, to find VSIs that are in either the dal05 or sng01 datacenter you can run this command: `slcli call-api Account getVirtualGuests -f 'virtualGuests.datacenter.name IN dal05,sng01'` + +### Changed + - CLI: Fixes a UnicodeEncodeError when piping slcli output with unicode characters. This was mostly reported with `slcli image list` but could also happen with many other calls. + - CLI: Fixed a bug where os_version was not displaying correctly in `slcli virtual detail` or `slcli virtual detail` + +## [5.1.0] - 2016-05-12 + - Changes: https://github.com/softlayer/softlayer-python/compare/v5.0.1...v5.1.0 + +### Added + - CLI+API: Added block storage functionality. You can order, list, detail, cancel volumes. You can list and delete snapshots. You can also list ACLs for volumes. + - Added functionality to attach/detach devices to tickets + - CLI: Virtual list now lists users and passwords for all known software + +### Changed + - CLI: Fixes bug with `vlan detail` CLI command + +## [5.0.1] - 2016-03-30 + - https://github.com/softlayer/softlayer-python/compare/v5.0.0...v5.0.1 + +### Changed + - CLI: Adds missing dependency that was previously pulled in by prompt_toolkit + - API: Fix a bug by updating the CDN manager to use the new purge method + - CLI: Fixes bug that occured when iscsi listings with resources have no datacenter + +## [5.0.0] - 2016-03-18 + - Changes: https://github.com/softlayer/softlayer-python/compare/v4.1.1...v5.0.0 + +### Added + - CLI: Adds a shell (accessable with `slcli shell`) which provides autocomplete for slcli commands and options + - CLI: How filters work with `slcli call-api` has changed significantly. Instead of accepting JSON, it now accepts an easier-to-use format. See `slcli call-api -h` for examples + - API: Adds manager for object storage + - API: Improved REST transport support + +### Changed + - CLI: Move modifying nic speed to `slcli virtual edit` and `slcli hardware edit` instead of having its own command + - CLI: 'virtual' and 'hardware' are preferred over 'vs' and 'server' in the CLI + - CLI+API: Many unmentioned bug fixes + +## [4.1.1] - 2015-08-17 + - Changes: https://github.com/softlayer/softlayer-python/compare/v4.1.0...v4.1.1 + +### Added + - CLI: Re-adds `--no-public` option to only provision private interfaces with servers via `slcli server create` + +### Changed + - CLI: Fixes to work with Click v5 + - Removes non-functional `--vlan-public` and `--vlan-private` from `slcli server create` + - VSManager.wait_for_ready will now behave as it is documented to behave. + +## [4.1.0] - 2015-08-17 + - Changes: https://github.com/softlayer/softlayer-python/compare/v4.0.4...v4.1.0 + +### Added + - CLI: Adds a shell which provides a shell interface for `slcli`. This is available by using `slcli shell` + - CLI: `slcli vs create` and `slcli server create` will now prompt for missing required options + - CLI+API: Adds editing of hardware tags + +### Changed + - CLI: Fixes `slcli firewall add` command + - CLI: Handles case where `slcli vs detail` and `slcli server detail` was causing an error when trying to display the creator + - API: Fixes VSManager.verify_create_instance() with tags (and, in turn, `slcli vs create --test` with tags) + - CLI: Fixes `vs resume` command + - API+CLI: Updates hardware ordering to deal with location-specific prices + - CLI: Fixes several description errors in the CLI + - CLI: Running `vs edit` without a tag option will no longer remove all tags + +## [4.0.4] - 2015-06-30 + - Changes: https://github.com/softlayer/softlayer-python/compare/v4.0.3...v4.0.4 + +### Changed + - CLI: Fixes bug with pulling the userData property for the virtual server detail + - CLI: Fixes a class of bugs invloving unicode from the API + +## [4.0.3] - 2015-06-15 + - Changes: https://github.com/softlayer/softlayer-python/compare/v4.0.2...v4.0.3 + +### Changed + - CLI: Fixes bug with `slcli vs ready` command + - CLI: Fixes bug with `slcli loadbal service-add` command + - CLI: Fixes bug with `slcli vlan list` with vlans that don't have a datacenter + - CLI: Improves validation of virtual server and hardware create commands + +## [4.0.2] - 2015-05-04 + - Changes https://github.com/softlayer/softlayer-python/compare/v4.0.1...v4.0.2 + +### Changed + - CLI: Fixes a bug that breaks user confirmation prompts + - CLI: Fixes general issue with sorting on certain row types in the CLI + - API: Fixes image capture for Windows guests + +## [4.0.1] - 2015-04-28 + - Changes: https://github.com/softlayer/softlayer-python/compare/v4.0.0...v4.0.1 + +### Changed + - CLI: Fixes bug in `sl setup` command not properly defaulting to current values. + - API: Fixes bug where turning off compression headers would still send compression headers. + - CLI: Reverts to using ids over global identifiers for `sl vs list` and `sl server list`. + +## [4.0.0] - 2015-04-21 + - Changes: https://github.com/softlayer/softlayer-python/compare/v3.3.0...v4.0.0 + - Because there are many changes between version 3 and version 4, it is strongly recommend to pin the version of the SoftLayer python bindings as soon as you can in order to prevent unintentional breakage when upgrading. To keep yourself on version 3, you can use this directive: softlayer>=3,<4. That can be used with pip (pip install softlayer>=3,<4), requirements in your setup.py and/or in your requirements.txt file. + +### Added + - API: The client transport is now pluggable. If you want to add extra logging or accounting, you can now subclass or wrap softlayer.transports.XmlRpcTransport in order to do so. A good example of that is done with softlayer.transports.TimingTransport. + - API+CLI: Adds ability to import virtual images from a given URI. The API only supports importing from a swift account using 'swift://'. For more details, see http://developer.softlayer.com/reference/services/SoftLayer_Virtual_Guest_Block_Device_Template_Group/createFromExternalSource. + - CLI: A `--fixtures` global flag was added to pull from fixture data instead of the API. This is useful for discovery, demonstration and testing purposes. + - CLI: A `--verbose` or `-v` flag was added to eventually replace `--debug`. To make a command more verbose, simply add more `-v` flags. For example `sl -vvv vs list` will be the most verbose and show everything down to request/response tracing. + - CLI: Credentials can now be requested using `sl vs credentials `, `sl hardware credentials ` and `sl nas credentials ` for virtual servers, hardware servers and NAS accounts respectively. + - CLI: Adds virtual server rescue command, `sl vs rescue ` + +### Changed + - CLI: The command is renamed from `sl` to `slcli` to avoid package conflicts. + - CLI: Global options now need to be specified right after the `slcli` command. For example, you would now use `slcli --format=raw list` over `slcli vs list --format=raw`. This is a change for the following options: + - --format + - -c or --config + - --debug + - --proxy + - -y or --really + - --version + - API: The hardware manager has a significant update to how place_order() works. It will now only support the fast server provisioning package which has presets for options like CPU, Memory and disk. + - API: Removed deprecated SoftLayer.CCIManager. + - API: Adds virtual server rescue command to SoftLayer.VSManager + - CLI: Significant changes were done to the CLI argument parsing. Docopt was dropped in favor of click. Therefore, some subtle differences which aren't documented here may exist. + +## [3.3.0] - 2014-10-23 + - Changes: https://github.com/softlayer/softlayer-python/compare/v3.2.0...v3.3.0 + +### Added + - CLI+API: Load balancer support + - CLI: More detail added to the `sl image detail` and `sl image list` commands + - CLI: Adds command to import DNS entries from BIND zone files + - CLI+API: Adds support for booting into rescue images for virtual servers and hardware + - API: Adds ability to order virtual and hardare servers from a quote to the ordering manager + +### Changed + - CLI: Fixes bug with `sl server list-chassis` and `sl server list-chassis` + - API: Restructure of the way custom authentication can be plugged in the API client + - Several other bug fixes + +## [3.2.0] - 2014-07-09 + - Changes: https://github.com/softlayer/softlayer-python/compare/v3.1.0...v3.2.0 + +### Added + - CLI+API: Added firewall manager and CLI module + - CLI+API: Added iscsi manager and CLI module + - API: Added ability to create multiple virtual servers at once to VSManager + - API: Added OrderingManager. Remove hard-coded price IDs + +### Changed + - Fixed several small bugs + +## [3.1.0] - 2014-04-24 + - Changes: https://github.com/softlayer/softlayer-python/compare/v3.0.2...v3.1.0 + +### Added + - CLI+API: Added CDN manager and CLI module + - CLI+API: Added ticket manager and CLI module + - CLI+API: Added image manager and improves image CLI module + - CLI+API: Added the ability to specify a proxy URL for API bindings and the CLI + - CLI+API: Added ability to resize a virtual machine + - CLI+API: Added firewall manager and CLI module + - CLI+API: Added load balancer manager and CLI module + +### Changed + - API: six is now used to provide support for Python 2 and Python 3 with the same source + - CLI+API: Implemented product name changes in accordance with SoftLayer's new product names. Existing managers should continue to work as before. Minor CLI changes were necessary. + - Many bug fixes and minor suggested improvements + +## [3.0.2] - 2013-12-9 + - Changes: https://github.com/softlayer/softlayer-python/compare/v3.0.1...v3.0.2 + +### Added + - CLI+API: Simplified object mask reformatting and added support for more complex masks. + - CLI+API: Added IPMI IP address to hardware details. + - CLI: Added support for ordering multiple disks when creating a CCI. + - API: Added flag to disable compression on HTTP requests. + - CLI: Added CIDR information to subnet displays. + +### Changed + - CLI: Fixed the sl bmc create --network argument. + - CLI+API: Improved output of the message queue feature and fixed some minor bugs. + - CLI: Fixed an error when using --test and ordering a non-private subnet. + - API: Fix to prevent double counting results in summary_by_datacenter(). + +### [3.0.1] - 2013-10-11 + - Changes: https://github.com/softlayer/softlayer-python/compare/v3.0.0...v3.0.1 + +### Added + - CLI+API: Added ability to specify SSH keys when reloading CCIs and servers. + +### Changed + - CLI: Fixed an error message about pricing information that appeared when ordering a new private subnet. + +## [3.0.0] - 2013-09-19 + - Changes: https://github.com/softlayer/softlayer-python/compare/v2.3.0...v3.0.0 + +### Added + - CLI+API: Adds SoftLayer Message Queue Service bindings (as a manager) and a CLI counterpart. With this you can interact with existing message queue accounts + - CLI+API: Adds the ability to create CCIs with the following options: metadata, post-install script, SSH key + - CLI+API: Improved dedicated server ordering. Adds power management for hardware servers: power-on, power-off, power-cycle, reboot + - CLI+API: Adds a networking manager and adds several network-related CLI modules. This includes the ability to: + - list, create, cancel and assign global IPs + - list, create, cancel and detail subnets. Also has the ability to lookup details about an IP address with 'sl subnet lookup' + - list, detail VLANs + - show and edit RWhois data + - CLI+API: Ability to manage SSH Keys with a manager and a CLI module + - CLI: Adds a --debug option to print out debugging information. --debug=3 is the highest log level which prints full HTTP request/responses including the body + - CLI+API: Adds the ability to create hardware servers with a default SSH key + - CLI: Adds templating for creating CCIs and hardware nodes which can be used to create more CCIs and hardware with the same settings + +### Changed + - Many bug fixes and consistency improvements + - API: Removes old API client interfaces which have been deprecated in the v2. See link for more details: https://softlayer-api-python-client.readthedocs.org/en/latest/api/client/#backwards-compatibility + - CLI: The commands in the main help are now organized into categories + +## [2.3.0] - 2013-07-19 + - Changes: https://github.com/softlayer/softlayer-python/compare/v2.2.0...v2.3.0 + +### Added + - CLI+API: Added much more hardware support: Filters for hardware listing, dedicated server/bare metal cloud ordering, hardware cancellation + - CLI+API: Added DNS Zone filtering (server side) + - CLI+API: Added Post Install script support for CCIs and hardware + - CLI: Added Message queue functionality + - CLI: Added --debug option to CLI commands + - API: Added more logging + - API: Added token-based auth so you can use the API bindings with your username/password if you want. (It's still highly recommended to use your API key instead of your password) + +### Changed + - Several bug fixes and improvements + - Removed Python 2.5 support. Some stuff MIGHT work with 2.5 but it is no longer tested + - API: Refactored managers into their own module to not clutter the top level + +## [2.2.0] - 2013-04-11 +### Added + - Added sphinx documentation. See it here: https://softlayer-api-python-client.readthedocs.org + - CCI: Adds Support for Additional Disks + - CCI: Adds a way to block until transactions are done on a CCI + - CLI: For most CCI commands, you can specify id, hostname, private ip or public ip as + - CLI: Adds the ability to filter list results for CCIs + - API: for large result sets, requests can now be chunked into smaller batches on the server side. Using service.iter_call('getObjects', ...) or service.getObjects(..., iter=True) will return a generator regardless of the results returned. offset and limit can be passed in like normal. An additional named parameter of 'chunk' is used to limit the number of items coming back in a single request, defaults to 100 + +### Changed + - Consistency changes/bug fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a82eb151..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/cla-individual.md). +## Procedural -* If you're contributing on behalf of your employer we'll need a signed copy of our corporate contributor agreement (CCLA) as well. You can find the [CCLA here](./docs/cla-corporate.md). - -* Fork the repo, make your changes, and open a pull request. +1. All code changes require a corresponding issue. [Open an issue here](https://github.com/softlayer/softlayer-python/issues). +2. Fork the [softlayer-python](https://github.com/softlayer/softlayer-python) repository. +3. Make any changes required, commit messages should reference the issue number (include #1234 if the message if your issue is number 1234 for example). +4. Make a pull request from your fork/branch to origin/master +5. Requires 1 approval for merging * Additional infomration can be found in our [contribution guide](http://softlayer-python.readthedocs.org/en/latest/dev/index.html) +## Legal + +* See our [Contributor License Agreement](./docs/dev/cla-individual.md). Opening a pull request is acceptance of this agreement. + +* If you're contributing on behalf of your employer we'll need a signed copy of our corporate contributor agreement (CCLA) as well. You can find the [CCLA here](./docs/dev/cla-corporate.md). + + +## Code style + +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 + + def test_test(self): + amock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + amock.return_value = fixtures.SoftLayer_Product_Package.RESERVED_CAPACITY +``` + +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 88442234c..5b475dc73 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,11 +6,13 @@ chechuironman Christopher Gallo David Ibarra Hans Kristian Moen +Ian Sutton Jake Williams Jason Johnson Kevin Landreth Kevin McDonald Łukasz Oleś +Michael Fork Nathan Beittenmiller Neetu Jain Paul Sroufe @@ -20,6 +22,7 @@ Ryan Hanson Scott Thompson Sergio Carlos Shane Poage +Shravan Kumar Raghu simplydave SoftLayer suppandi @@ -27,3 +30,4 @@ Swapnil Khanapurkar The SoftLayer Developer Network Tim Ariyeh Wissam Elriachy +Anthony Monthe (ZuluPro) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE new file mode 100644 index 000000000..bad173543 --- /dev/null +++ b/ISSUE_TEMPLATE @@ -0,0 +1,9 @@ +**Please triple-check to make sure that you have properly masked out user credentials like usernames, passwords and API keys before submitting your issue** + +### Expected Behavior + +### Actual Behavior + +### Environment Information +Operating System: +softlayer-python version (`slcli --version`): diff --git a/LICENSE b/LICENSE index e606a227d..e6da61830 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +Copyright (c) 2016 SoftLayer Technologies, Inc. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. 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 1e30294cc..29536d085 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,20 @@ 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/softlayer/softlayer-python/badge.svg - :target: https://coveralls.io/r/softlayer/softlayer-python - +.. 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. @@ -22,15 +22,16 @@ SoftLayer products and services. Documentation ------------- -Documentation for the Python client is available at -http://softlayer.github.io/softlayer-python/. +Documentation for the Python client is available at `Read the Docs `_ . Additional API documentation can be found on the SoftLayer Development Network: * `SoftLayer API reference - `_ + `_ * `Object mask information and examples - `_ + `_ +* `Code Examples + `_ Installation ------------ @@ -47,24 +48,142 @@ 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. Please post to Stack Overflow at https://stackoverflow.com/ or open a support ticket in the customer portal if you have any questions regarding use of this library. If you use Stack Overflow please tag your posts with “SoftLayer” so our team can easily find your post. +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. 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, or 3.4. +* 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 +--------------- +* 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) 2015 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 new file mode 100644 index 000000000..962ee1663 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,74 @@ + + +# 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 dc887d41b..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,6 +46,7 @@ 'raw_headers', 'limit', 'offset', + 'verify' )) @@ -45,7 +58,8 @@ def create_client_from_env(username=None, config_file=None, proxy=None, user_agent=None, - transport=None): + transport=None, + verify=True): """Creates a SoftLayer API client using your environment. Settings are loaded via keyword arguments, environemtal variables and @@ -67,6 +81,8 @@ def create_client_from_env(username=None, calls if you wish to bypass the packages built in User Agent string :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) + :param bool verify: decide to verify the server's SSL/TLS cert. DO NOT SET + TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS. Usage: @@ -77,21 +93,36 @@ 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, timeout=timeout, proxy=proxy, + verify=verify, config_file=config_file) - # Default the transport to use XMLRPC if transport is None: - transport = transports.XmlRpcTransport( - endpoint_url=settings.get('endpoint_url'), - proxy=settings.get('proxy'), - timeout=settings.get('timeout'), - user_agent=user_agent, - ) + url = settings.get('endpoint_url') + if url is not None and '/rest' in url: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + endpoint_url=settings.get('endpoint_url'), + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + else: + # Default the transport to use XMLRPC + transport = transports.XmlRpcTransport( + endpoint_url=settings.get('endpoint_url'), + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) # If we have enough information to make an auth driver, let's do it if auth is None and settings.get('username') and settings.get('api_key'): @@ -111,16 +142,91 @@ def create_client_from_env(username=None, settings.get('api_key'), ) - return BaseClient(auth=auth, transport=transport) + return BaseClient(auth=auth, transport=transport, config_file=config_file) -def Client(**kwargs): - """Get a SoftLayer API Client using environmental settings. +def employee_client(username=None, + access_token=None, + endpoint_url=None, + timeout=None, + auth=None, + config_file=None, + proxy=None, + user_agent=None, + transport=None, + verify=True): + """Creates an INTERNAL SoftLayer API client using your environment. + + Settings are loaded via keyword arguments, environemtal variables and config file. - Deprecated in favor of create_client_from_env() + :param username: your user ID + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token) + :param password: password to use for employee authentication + :param endpoint_url: the API endpoint base URL you wish to connect to. + Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. + :param proxy: proxy to be used to make API calls + :param integer timeout: timeout for API requests + :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. + Example: `BasicAuthentication` + :param config_file: A path to a configuration file used to load settings + :param user_agent: an optional User Agent to report when making API + calls if you wish to bypass the packages built in User Agent string + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) + :param bool verify: decide to verify the server's SSL/TLS cert. """ - warnings.warn("use SoftLayer.create_client_from_env() instead", - DeprecationWarning) + settings = config.get_client_settings(username=username, + api_key=None, + endpoint_url=endpoint_url, + timeout=timeout, + proxy=proxy, + verify=None, + config_file=config_file) + + url = settings.get('endpoint_url', '') + verify = settings.get('verify', True) + + if 'internal' not in url: + raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.") + + if transport is None: + if url is not None and '/rest' in url: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + else: + # Default the transport to use XMLRPC + transport = transports.XmlRpcTransport( + endpoint_url=url, + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + + if access_token is None: + access_token = settings.get('access_token') + + user_id = settings.get('userid') + # Assume access_token is valid for now, user has logged in before at least. + if settings.get('auth_cert', False): + auth = slauth.X509Authentication(settings.get('auth_cert'), verify) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + elif access_token and user_id: + auth = slauth.EmployeeAuthentication(user_id, access_token) + return EmployeeClient(auth=auth, transport=transport, config_file=config_file) + else: + # This is for logging in mostly. + LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.") + return EmployeeClient(auth=None, transport=transport, config_file=config_file) + + +def Client(**kwargs): + """Get a SoftLayer API Client using environmental settings.""" return create_client_from_env(**kwargs) @@ -128,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 @@ -174,15 +315,21 @@ def __getitem__(self, name): return Service(self, name) def call(self, service, method, *args, **kwargs): - """Make a SoftLayer API call + """Make a SoftLayer API call. - :param service: the name of the SoftLayer API service :param method: the method to call on the service - :param \\*args: same optional arguments that ``Service.call`` takes - :param \\*\\*kwargs: same optional keyword arguments that - ``Service.call`` takes - - :param service: the name of the SoftLayer API service + :param \\*args: (optional) arguments for the remote call + :param id: (optional) id for the resource + :param mask: (optional) object mask + :param dict filter: (optional) filter dict + :param dict headers: (optional) optional XML-RPC headers + :param boolean compress: (optional) Enable/Disable HTTP compression + :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 bool verify: verify SSL cert + :param cert: client certificate path Usage: >>> import SoftLayer @@ -192,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': '*/*'} @@ -222,15 +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', {})) @@ -243,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 + + 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 len(results) < chunk: - break + 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) @@ -302,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. @@ -309,27 +781,21 @@ class Service(object): :param name str: The service name """ + def __init__(self, client, name): self.client = client self.name = name def call(self, name, *args, **kwargs): - """Make a SoftLayer API call. + """Make a SoftLayer API call + :param service: the name of the SoftLayer API service :param method: the method to call on the service - :param \\*args: (optional) arguments for the remote call - :param id: (optional) id for the resource - :param mask: (optional) object mask - :param dict filter: (optional) filter dict - :param dict headers: (optional) optional XML-RPC headers - :param boolean compress: (optional) Enable/Disable HTTP compression - :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 bool verify: verify SSL cert - :param cert: client certificate path + :param \\*args: same optional arguments that ``BaseClient.call`` takes + :param \\*\\*kwargs: same optional keyword arguments that + ``BaseClient.call`` takes + + :param service: the name of the SoftLayer API service Usage: >>> import SoftLayer 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/__init__.py b/SoftLayer/CLI/block/__init__.py new file mode 100644 index 000000000..7ccee03c6 --- /dev/null +++ b/SoftLayer/CLI/block/__init__.py @@ -0,0 +1 @@ +"""Block Storage.""" diff --git a/SoftLayer/CLI/block/access/__init__.py b/SoftLayer/CLI/block/access/__init__.py new file mode 100644 index 000000000..050b3938d --- /dev/null +++ b/SoftLayer/CLI/block/access/__init__.py @@ -0,0 +1 @@ +"""Block Storage Access Control.""" diff --git a/SoftLayer/CLI/block/access/authorize.py b/SoftLayer/CLI/block/access/authorize.py new file mode 100644 index 000000000..4a5931e95 --- /dev/null +++ b/SoftLayer/CLI/block/access/authorize.py @@ -0,0 +1,47 @@ +"""Authorizes hosts on a specific block volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions + +MULTIPLE = '(Multiple allowed)' + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume_id') +@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. ' + MULTIPLE) +@click.option('--ip-address', multiple=True, + 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): + """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) + + # Convert actual IP Addresses to their SoftLayer ids + if ip_address is not None: + network_manager = SoftLayer.NetworkManager(env.client) + 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.") + raise exceptions.ArgumentError('Incorrect IP Address') + 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) + + # 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 new file mode 100644 index 000000000..c2aa4c243 --- /dev/null +++ b/SoftLayer/CLI/block/access/list.py @@ -0,0 +1,47 @@ +"""List hosts with access to block volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +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(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume_id') +@click.option('--columns', + callback=column_helper.get_formatter(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 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=resolved_id) + table = formatting.Table(columns.columns) + table.sortby = sortby + + for key, type_name in [('allowedVirtualGuests', 'VIRTUAL'), + ('allowedHardware', 'HARDWARE'), + ('allowedSubnets', 'SUBNET'), + ('allowedIpAddresses', 'IP')]: + for obj in access_list.get(key, []): + obj['type'] = type_name + table.add_row([value or formatting.blank() + for value in columns.row(obj)]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/access/password.py b/SoftLayer/CLI/block/access/password.py new file mode 100644 index 000000000..744c5c5ab --- /dev/null +++ b/SoftLayer/CLI/block/access/password.py @@ -0,0 +1,27 @@ +"""Modifies a password for a volume's access""" +# :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') +@click.option('--password', '-p', multiple=False, + 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. + + access id is the allowed_host_id from slcli block access-list + """ + + block_manager = SoftLayer.BlockStorageManager(env.client) + + result = block_manager.set_credential_password(access_id=access_id, password=password) + + if result: + click.echo('Password updated for %s' % access_id) + else: + click.echo('FAILED updating password for %s' % access_id) diff --git a/SoftLayer/CLI/block/access/revoke.py b/SoftLayer/CLI/block/access/revoke.py new file mode 100644 index 000000000..d1ab27bd6 --- /dev/null +++ b/SoftLayer/CLI/block/access/revoke.py @@ -0,0 +1,44 @@ +"""Revokes hosts' access on a specific block 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('--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.') +@click.option('--ip-address', multiple=True, + 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): + """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) + + # Convert actual IP Addresses to their SoftLayer ids + if ip_address is not None: + network_manager = SoftLayer.NetworkManager(env.client) + for ip_address_value in ip_address: + ip_address_object = network_manager.ip_lookup(ip_address_value) + ip_address_id_list.append(ip_address_object['id']) + + block_manager.deauthorize_host_to_volume(volume_id, + hardware_id, + virtual_id, + ip_address_id_list) + + # If no exception was raised, the command succeeded + click.echo('Access to %s was revoked for the specified hosts' % volume_id) diff --git a/SoftLayer/CLI/block/cancel.py b/SoftLayer/CLI/block/cancel.py new file mode 100644 index 000000000..3d25cc27d --- /dev/null +++ b/SoftLayer/CLI/block/cancel.py @@ -0,0 +1,47 @@ +"""Cancel an existing iSCSI account.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('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, 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 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) + + if cancelled: + if immediate: + click.echo('Block volume with id %s has been marked' + ' for immediate cancellation' % volume_id) + else: + click.echo('Block volume with id %s has been marked' + ' for cancellation' % volume_id) + else: + click.echo('Unable to cancel block volume %s' % volume_id) 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 new file mode 100644 index 000000000..f06c6c935 --- /dev/null +++ b/SoftLayer/CLI/block/count.py @@ -0,0 +1,42 @@ +"""List number of block storage volumes 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', + 'Count' +] + + +@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 +def cli(env, sortby, datacenter): + """List number of block storage volumes per datacenter.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + mask = "mask[serviceResource[datacenter[name]],"\ + "replicationPartners[serviceResource[datacenter[name]]]]" + block_volumes = block_manager.list_block_volumes(datacenter=datacenter, + mask=mask) + + # cycle through all block volumes and count datacenter occurences. + 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(): # pylint: disable=consider-iterating-dictionary + datacenters[datacenter_name] = 1 + else: + datacenters[datacenter_name] += 1 + + table = formatting.KeyValueTable(DEFAULT_COLUMNS) + table.sortby = sortby + 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 new file mode 100644 index 000000000..a4359fae3 --- /dev/null +++ b/SoftLayer/CLI/block/detail.py @@ -0,0 +1,93 @@ +"""Display details for a specified volume.""" +# :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 +from SoftLayer import utils + + +@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_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)', capacity]) + table.add_row(['LUN Id', block_volume['lunId']]) + + 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']]) + + if 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(['# of Active Transactions', block_volume['activeTransactionCount']]) + + if block_volume['activeTransactions']: + for trans in block_volume['activeTransactions']: + if 'transactionStatus' in trans and 'friendlyName' in trans['transactionStatus']: + table.add_row(['Ongoing Transaction', trans['transactionStatus']['friendlyName']]) + + table.add_row(['Replicant Count', 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', block_volume['replicationStatus']['message']]) + else: + table.add_row(['Replication Status', block_volume['replicationStatus']]) + + replicant_table = formatting.Table(['Id', 'Username', 'Target', 'Location', 'Schedule']) + replicant_table.align['Name'] = 'r' + replicant_table.align['Value'] = 'l' + for replicant in block_volume['replicationPartners']: + replicant_table.add_row([ + replicant.get('id'), + utils.lookup(replicant, 'username'), + utils.lookup(replicant, 'serviceResourceBackendIpAddress'), + utils.lookup(replicant, 'serviceResource', 'datacenter', 'name'), + utils.lookup(replicant, 'replicationSchedule', 'type', 'keyname') + ]) + table.add_row(['Replicant Volumes', replicant_table]) + + if block_volume.get('originalVolumeSize'): + original_volume_info = formatting.Table(['Property', 'Value']) + 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 new file mode 100644 index 000000000..44a077bc0 --- /dev/null +++ b/SoftLayer/CLI/block/duplicate.py @@ -0,0 +1,103 @@ +"""Order a duplicate 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('origin-volume-id') +@click.option('--origin-snapshot-id', '-o', + type=int, + help="ID of an origin volume snapshot to use for duplcation.") +@click.option('--duplicate-size', '-c', + type=int, + help='Size of duplicate block volume in GB. ' + '***If no size is specified, the size of ' + '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]') +@click.option('--duplicate-iops', '-i', + type=int, + help='Performance Storage IOPS, between 100 and 6000 in ' + 'multiples of 100 [only used for performance volumes] ' + '***If no IOPS value is specified, the IOPS value of the ' + 'origin volume will be used.***\n' + 'Requirements: [If IOPS/GB for the origin volume is less ' + 'than 0.3, IOPS/GB for the duplicate must also be less ' + 'than 0.3. If IOPS/GB for the origin volume is greater ' + 'than or equal to 0.3, IOPS/GB for the duplicate must ' + 'also be greater than or equal to 0.3.]') +@click.option('--duplicate-tier', '-t', + help='Endurance Storage Tier (IOPS per GB) [only used for ' + 'endurance volumes] ***If no tier is specified, the tier ' + 'of the origin volume will be used.***\n' + 'Requirements: [If IOPS/GB for the origin volume is 0.25, ' + 'IOPS/GB for the duplicate must also be 0.25. If IOPS/GB ' + 'for the origin volume is greater than 0.25, IOPS/GB ' + 'for the duplicate must also be greater than 0.25.]', + type=click.Choice(['0.25', '2', '4', '10'])) +@click.option('--duplicate-snapshot-size', '-s', + type=int, + help='The size of snapshot space to order for the duplicate. ' + '***If no snapshot space size is specified, the snapshot ' + 'space size of the origin block volume will be used.***\n' + 'Input "0" for this parameter to order a duplicate volume ' + 'with no snapshot space.') +@click.option('--billing', + 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, + 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 + if billing.lower() == "hourly": + hourly_billing_flag = True + + if duplicate_tier is not None: + duplicate_tier = float(duplicate_tier) + + try: + order = block_manager.order_duplicate_volume( + origin_volume_id, + origin_snapshot_id=origin_snapshot_id, + duplicate_size=duplicate_size, + duplicate_iops=duplicate_iops, + duplicate_tier_level=duplicate_tier, + duplicate_snapshot_size=duplicate_snapshot_size, + 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(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/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 new file mode 100644 index 000000000..9a65603e0 --- /dev/null +++ b/SoftLayer/CLI/block/list.py @@ -0,0 +1,83 @@ +"""List block storage volumes.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import storage_utils + +COLUMNS = [ + column_helper.Column('id', ('id',), mask="id"), + column_helper.Column('username', ('username',), mask="username"), + column_helper.Column('datacenter', + ('serviceResource', 'datacenter', 'name'), + mask="serviceResource.datacenter.name"), + column_helper.Column( + '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) + else '-', + mask="storageType.keyName"), + column_helper.Column('capacity_gb', ('capacityGb',), mask="capacityGb"), + column_helper.Column('IOPs', ('provisionedIops',), mask="provisionedIops"), + column_helper.Column('ip_addr', ('serviceResourceBackendIpAddress',), + mask="serviceResourceBackendIpAddress"), + column_helper.Column('lunId', ('lunId',), mask="lunId"), + column_helper.Column('active_transactions', ('activeTransactionCount',), + mask="activeTransactionCount"), + column_helper.Column('rep_partner_count', ('replicationPartnerCount',), + mask="replicationPartnerCount"), + column_helper.Column( + 'created_by', + ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), + column_helper.Column('notes', ('notes',), mask="notes"), +] + +DEFAULT_COLUMNS = [ + 'id', + 'username', + 'datacenter', + 'storage_type', + 'capacity_gb', + 'IOPs', + 'ip_addr', + 'lunId', + 'active_transactions', + 'rep_partner_count', + 'notes' +] + + +@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=f"Columns to display. Options: {', '.join(column.name for column in COLUMNS)}", + default=','.join(DEFAULT_COLUMNS)) +@environment.pass_env +def cli(env, sortby, columns, datacenter, username, storage_type, order): + """List block storage. + + 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 = 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 new file mode 100644 index 000000000..3a677fd5a --- /dev/null +++ b/SoftLayer/CLI/block/lun.py @@ -0,0 +1,36 @@ +"""Set the LUN ID on an iSCSI 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.argument('lun-id') +@environment.pass_env +def cli(env, volume_id, lun_id): + """Set the LUN ID on an existing block storage volume. + + The LUN ID only takes effect during the Host Authorization process. It is + recommended (but not necessary) to de-authorize all hosts before using this + method. See `block access-revoke`. + + VOLUME_ID - the volume ID on which to set the LUN ID. + + LUN_ID - recommended range is an integer between 0 and 255. Advanced users + can use an integer between 0 and 4095. + """ + + block_storage_manager = SoftLayer.BlockStorageManager(env.client) + + res = block_storage_manager.create_or_update_lun_id(volume_id, lun_id) + + if 'value' in res and lun_id == res['value']: + click.echo( + 'Block volume with id %s is reporting LUN ID %s' % (res['volumeId'], res['value'])) + else: + click.echo( + 'Failed to confirm the new LUN ID on volume %s' % (volume_id)) 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 new file mode 100644 index 000000000..bdd0100e0 --- /dev/null +++ b/SoftLayer/CLI/block/order.py @@ -0,0 +1,133 @@ +"""Order a 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.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.', + required=True) +@click.option('--iops', + type=int, + 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]', + type=click.Choice(['0.25', '2', '4', '10'])) +@click.option('--os-type', + help='Operating System', + type=click.Choice([ + 'HYPER_V', + 'LINUX', + 'VMWARE', + 'WINDOWS_2008', + 'WINDOWS_GPT', + 'WINDOWS', + 'XEN']), + required=True) +@click.option('--location', + help='Datacenter short name (e.g.: dal09)', + required=True) +@click.option('--snapshot-size', + type=int, + help='Optional parameter for ordering snapshot ' + '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\']. enterprise and performance are depreciated""", + default='storage_as_a_service', + type=click.Choice([ + 'storage_as_a_service', + 'enterprise', + 'performance'])) +@click.option('--billing', + type=click.Choice(['hourly', 'monthly']), + default='monthly', + help="Optional parameter for Billing rate (default to monthly)") +@environment.pass_env +def cli(env, storage_type, size, iops, tier, os_type, + location, snapshot_size, service_offering, billing): + """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() + + hourly_billing_flag = False + if billing.lower() == "hourly": + hourly_billing_flag = True + + 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 service_offering == 'performance' and snapshot_size is not None: + raise exceptions.CLIAbort( + '--snapshot-size is not available for performance service offerings. ' + 'Use --service-offering storage_as_a_service' + ) + + try: + order = block_manager.order_block_volume( + storage_type=storage_type, + location=location, + size=int(size), + iops=iops, + os_type=os_type, + snapshot_size=snapshot_size, + service_offering=service_offering, + hourly_billing_flag=hourly_billing_flag + ) + except ValueError as ex: + raise exceptions.ArgumentError(str(ex)) + + if storage_type == 'endurance': + if tier is None: + raise exceptions.CLIAbort( + 'Option --tier required with Endurance in IOPS/GB [0.25,2,4,10]' + ) + + try: + order = block_manager.order_block_volume( + storage_type=storage_type, + location=location, + size=int(size), + tier_level=float(tier), + os_type=os_type, + snapshot_size=snapshot_size, + service_offering=service_offering, + hourly_billing_flag=hourly_billing_flag + ) + 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']) + click.echo(f"\nYou may run 'slcli block volume-list --order {order['placedOrder']['id']}' " + "to find this block volume after it is ready.") + else: + click.echo("Order could not be placed! Please verify your options and try again.") diff --git a/SoftLayer/CLI/block/refresh.py b/SoftLayer/CLI/block/refresh.py new file mode 100644 index 000000000..e58fbf28d --- /dev/null +++ b/SoftLayer/CLI/block/refresh.py @@ -0,0 +1,26 @@ +"""Refresh a duplicate volume with a snapshot from its parent.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume_id') +@click.argument('snapshot_id') +@click.option('--force-refresh', '-f', is_flag=True, default=False, show_default=True, + help="Cancel current refresh process and initiates the new refresh.") +@environment.pass_env +def cli(env, volume_id, snapshot_id, force_refresh): + """Refresh a duplicate volume with a snapshot from its parent. + + EXAMPLE:: + + slcli block volume-refresh VOLUME_ID SNAPSHOT_ID + Refresh a duplicate VOLUME_ID with a snapshot from its parent SNAPSHOT_ID. + """ + block_manager = SoftLayer.BlockStorageManager(env.client) + resp = block_manager.refresh_dupe(volume_id, snapshot_id, force_refresh) + + click.echo(resp) diff --git a/SoftLayer/CLI/block/replication/__init__.py b/SoftLayer/CLI/block/replication/__init__.py new file mode 100644 index 000000000..ac0a44b7e --- /dev/null +++ b/SoftLayer/CLI/block/replication/__init__.py @@ -0,0 +1 @@ +"""Block Storage Replication Control.""" 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 new file mode 100644 index 000000000..e7f4dc737 --- /dev/null +++ b/SoftLayer/CLI/block/replication/failback.py @@ -0,0 +1,21 @@ +"""Failback from a replicant 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): + """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) + + if success: + click.echo("Failback from replicant is now in progress.") + else: + click.echo("Failback operation could not be initiated.") diff --git a/SoftLayer/CLI/block/replication/failover.py b/SoftLayer/CLI/block/replication/failover.py new file mode 100644 index 000000000..1bb63f84f --- /dev/null +++ b/SoftLayer/CLI/block/replication/failover.py @@ -0,0 +1,25 @@ +"""Failover to a replicant 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('--replicant-id', help="ID of the replicant volume.") +@environment.pass_env +def cli(env, volume_id, replicant_id): + """Failover a block volume to the given replica volume.""" + block_storage_manager = SoftLayer.BlockStorageManager(env.client) + + success = block_storage_manager.failover_to_replicant( + volume_id, + replicant_id + ) + + if success: + click.echo("Failover to replicant is now in progress.") + else: + click.echo("Failover operation could not be initiated.") diff --git a/SoftLayer/CLI/block/replication/locations.py b/SoftLayer/CLI/block/replication/locations.py new file mode 100644 index 000000000..0767d0811 --- /dev/null +++ b/SoftLayer/CLI/block/replication/locations.py @@ -0,0 +1,48 @@ +"""List suitable replication datacenters for the given volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +COLUMNS = [ + column_helper.Column('ID', ('id',), mask="id"), + column_helper.Column('Long Name', ('longName',), mask="longName"), + column_helper.Column('Short Name', ('name',), mask="name"), +] + +DEFAULT_COLUMNS = [ + 'ID', + 'Long Name', + 'Short Name', +] + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume-id') +@click.option('--columns', + callback=column_helper.get_formatter(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.""" + block_storage_manager = SoftLayer.BlockStorageManager(env.client) + + legal_centers = block_storage_manager.get_replication_locations( + volume_id + ) + + if not legal_centers: + click.echo("No data centers compatible for replication.") + else: + table = formatting.KeyValueTable(columns.columns) + table.sortby = sortby + for legal_center in legal_centers: + table.add_row([value or formatting.blank() + for value in columns.row(legal_center)]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/replication/order.py b/SoftLayer/CLI/block/replication/order.py new file mode 100644 index 000000000..e3c666f2a --- /dev/null +++ b/SoftLayer/CLI/block/replication/order.py @@ -0,0 +1,78 @@ +"""Order a block storage replica 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 helpers +from SoftLayer import utils + + +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('--datacenter', '-d', + help='Short name of the datacenter for the replica (e.g.: dal09)', + required=True) +@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', + 'VMWARE', + 'WINDOWS_2008', + '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, 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( + block_volume_id, + snapshot_schedule=snapshot_schedule, + 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(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 new file mode 100644 index 000000000..c5ae5075b --- /dev/null +++ b/SoftLayer/CLI/block/replication/partners.py @@ -0,0 +1,40 @@ +"""List existing replicant volumes for a block volume.""" +# :license: MIT, see LICENSE for more details. + +import click +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 = storage_utils.REPLICATION_PARTNER_COLUMNS +DEFAULT_COLUMNS = storage_utils.REPLICATION_PARTNER_DEFAULT + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume-id') +@click.option('--columns', + callback=column_helper.get_formatter(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 replica volumes for a block volume.""" + block_storage_manager = SoftLayer.BlockStorageManager(env.client) + + legal_volumes = block_storage_manager.get_replication_partners( + volume_id + ) + + if not legal_volumes: + click.echo("There are no replication partners for the given volume.") + else: + table = formatting.Table(columns.columns) + table.sortby = sortby + for legal_volume in legal_volumes: + table.add_row([value or formatting.blank() + for value in columns.row(legal_volume)]) + + env.fout(table) 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/__init__.py b/SoftLayer/CLI/block/snapshot/__init__.py new file mode 100644 index 000000000..eb4d41b07 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/__init__.py @@ -0,0 +1 @@ +"""Block Storage Snapshot Control.""" diff --git a/SoftLayer/CLI/block/snapshot/cancel.py b/SoftLayer/CLI/block/snapshot/cancel.py new file mode 100644 index 000000000..575bbac04 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/cancel.py @@ -0,0 +1,46 @@ +"""Cancel a snapshot space subscription.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume-id') +@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.") +@click.option('--force', default=False, is_flag=True, help="Force modify") +@environment.pass_env +def cli(env, volume_id, reason, immediate, force): + """Cancel existing snapshot space for a given volume.""" + + 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("This action will incur charges on your account. Continue?")): + raise exceptions.CLIAbort('Aborted') + + cancelled = block_storage_manager.cancel_snapshot_space( + volume_id, reason, immediate) + + if cancelled: + if immediate: + click.echo('Block volume with id %s has been marked' + ' for immediate snapshot cancellation' % volume_id) + else: + click.echo('Block volume with id %s has been marked' + ' for snapshot cancellation' % volume_id) + else: + click.echo('Unable to cancel snapshot space for block volume %s' + % volume_id) diff --git a/SoftLayer/CLI/block/snapshot/create.py b/SoftLayer/CLI/block/snapshot/create.py new file mode 100644 index 000000000..92dbe4057 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/create.py @@ -0,0 +1,24 @@ +"""Create a block storage snapshot.""" +# :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('--notes', '-n', + help='Notes to set on the new snapshot.') +@environment.pass_env +def cli(env, volume_id, notes): + """Creates a snapshot on a given volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + snapshot = block_manager.create_snapshot(volume_id, notes=notes) + + if 'id' in snapshot: + click.echo('New snapshot created with id: %s' % snapshot['id']) + else: + click.echo('Error occurred while creating snapshot.\n' + 'Ensure volume is not failed over or in another ' + 'state which prevents taking snapshots.') diff --git a/SoftLayer/CLI/block/snapshot/delete.py b/SoftLayer/CLI/block/snapshot/delete.py new file mode 100644 index 000000000..4c08c59a7 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/delete.py @@ -0,0 +1,18 @@ +"""Delete a block storage snapshot.""" +# :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('snapshot_id') +@environment.pass_env +def cli(env, snapshot_id): + """Deletes a snapshot on a given volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + deleted = block_manager.delete_snapshot(snapshot_id) + + if deleted: + click.echo('Snapshot %s deleted' % snapshot_id) diff --git a/SoftLayer/CLI/block/snapshot/disable.py b/SoftLayer/CLI/block/snapshot/disable.py new file mode 100644 index 000000000..254b5bd1a --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/disable.py @@ -0,0 +1,28 @@ +"""Disable 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 exceptions + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume_id') +@click.option('--schedule-type', + 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.""" + + if (schedule_type not in ['INTERVAL', 'HOURLY', 'DAILY', 'WEEKLY']): + raise exceptions.CLIAbort( + '--schedule-type must be INTERVAL, HOURLY, DAILY, or WEEKLY') + + block_manager = SoftLayer.BlockStorageManager(env.client) + disabled = block_manager.disable_snapshots(volume_id, schedule_type) + + if disabled: + click.echo('%s snapshots have been disabled for volume %s' + % (schedule_type, volume_id)) diff --git a/SoftLayer/CLI/block/snapshot/enable.py b/SoftLayer/CLI/block/snapshot/enable.py new file mode 100644 index 000000000..ec54aa0c0 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/enable.py @@ -0,0 +1,61 @@ +# snapshot_enable.py +"""Create a block storage snapshot [ENABLE].""" + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume_id') +@click.option('--schedule-type', + help='Snapshot schedule [INTERVAL|HOURLY|DAILY|WEEKLY].', + required=True) +@click.option('--retention-count', + help='Number of snapshots to retain.', + required=True) +@click.option('--minute', + 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, integer between 0 to 23.', + default=0) +@click.option('--day-of-week', + 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.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + + 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 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') + if hour < 0 or hour > 23: + raise exceptions.CLIAbort( + '--hour value must be between 0 and 23') + if day_of_week not in valid_days: + raise exceptions.CLIAbort( + '--day_of_week value must be a valid day (ex: SUNDAY)') + + enabled = block_manager.enable_snapshots(volume_id, schedule_type, + retention_count, minute, + hour, day_of_week) + + if enabled: + click.echo('%s snapshots have been enabled for volume %s' + % (schedule_type, volume_id)) 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 new file mode 100644 index 000000000..e9eca5983 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/list.py @@ -0,0 +1,54 @@ +"""List block storage snapshots.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + + +COLUMNS = [ + column_helper.Column('id', ('id',), mask='id'), + column_helper.Column('name', ('notes',), mask='notes'), + column_helper.Column('created', ('snapshotCreationTimestamp',), + mask='snapshotCreationTimestamp'), + column_helper.Column('size_bytes', ('snapshotSizeBytes',), + mask='snapshotSizeBytes'), +] + +DEFAULT_COLUMNS = [ + 'id', + 'name', + 'created', + 'size_bytes' +] + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume_id') +@click.option('--sortby', help='Column to sort by.', + default='created') +@click.option('--columns', + callback=column_helper.get_formatter(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( + resolved_id, + mask=columns.mask() + ) + + table = formatting.Table(columns.columns) + table.sortby = sortby + + for snapshot in snapshots: + table.add_row([value or formatting.blank() + for value in columns.row(snapshot)]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/snapshot/order.py b/SoftLayer/CLI/block/snapshot/order.py new file mode 100644 index 000000000..a443c95ef --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/order.py @@ -0,0 +1,63 @@ +"""Order snapshot space for a block storage volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions + + +@click.command(cls=SoftLayer.CLI.command.SLCommand, ) +@click.argument('volume_id') +@click.option('--iops', + type=int, + 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].', + type=click.Choice(['0.25', '2', '4', '10'])) +@click.option('--upgrade', + type=bool, + help='Flag to indicate that the order is an upgrade.', + default=False, + is_flag=True) +@environment.pass_env +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=size, + tier=tier, + upgrade=upgrade, + iops=iops + ) + 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']) + if 'status' in order['placedOrder'].keys(): + click.echo(" > Order status: %s" % order['placedOrder']['status']) + else: + click.echo("Order could not be placed! Please verify your options " + + "and try again.") diff --git a/SoftLayer/CLI/block/snapshot/restore.py b/SoftLayer/CLI/block/snapshot/restore.py new file mode 100644 index 000000000..6cfc5591f --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/restore.py @@ -0,0 +1,22 @@ +"""Restore a block volume from a snapshot.""" +# :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('--snapshot-id', '-s', + help='The id of the snapshot which will be used' + ' to restore the block volume.') +@environment.pass_env +def cli(env, volume_id, snapshot_id): + """Restore block volume using a given snapshot.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + success = block_manager.restore_from_snapshot(volume_id, snapshot_id) + + if success: + click.echo('Block volume %s is being restored using snapshot %s' + % (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 11afe6839..e6c007e25 100644 --- a/SoftLayer/CLI/call_api.py +++ b/SoftLayer/CLI/call_api.py @@ -3,48 +3,178 @@ 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 +from SoftLayer.CLI import helpers +from SoftLayer import utils -# pylint: disable=unused-argument +SPLIT_TOKENS = [ + ('in', ' IN '), + ('eq', '='), +] -def validate_filter(ctx, param, value): - """Try to parse the given filter as a JSON string.""" - try: - if value: - return json.loads(value) - except ValueError: - raise click.BadParameter('filters need to be in JSON format') +def _build_filters(_filters): + """Builds filters using the filter options passed into the CLI. + This only supports the equals keyword at the moment. + """ + root = utils.NestedDict({}) + for _filter in _filters: + operation = None + for operation, token in SPLIT_TOKENS: + # split "some.key=value" into ["some.key", "value"] + top_parts = _filter.split(token, 1) + if len(top_parts) == 2: + break + else: + raise exceptions.CLIAbort('Failed to find valid operation for: %s' % _filter) + + key, value = top_parts + current = root + # split "some.key" into ["some", "key"] + parts = [part.strip() for part in key.split('.')] + + # Actually drill down and add the filter + for part in parts[:-1]: + current = current[part] + + if operation == 'eq': + current[parts[-1]] = utils.query_filter(value.strip()) + elif operation == 'in': + current[parts[-1]] = { + 'operation': 'in', + 'options': [{ + 'name': 'data', + 'value': [p.strip() for p in value.split(',')], + }], + } + + return root.to_dict() + + +def _build_python_example(args, kwargs): + sorted_kwargs = sorted(kwargs.items()) + + call_str = 'import SoftLayer\n\n' + call_str += 'client = SoftLayer.create_client_from_env()\n' + call_str += 'result = client.call(' + arg_list = [repr(arg) for arg in args] + arg_list += [key + '=' + repr(value) + for key, value in sorted_kwargs if value] + call_str += ',\n '.join(arg_list) + call_str += ')' + + return call_str + + +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}") -@click.command('call', short_help="Call arbitrary API endpoints.") + 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") -@click.option('--filter', '_filter', - callback=validate_filter, - help="Object filter in a JSON string") +@helpers.multi_option('--filter', '-f', '_filters', + 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, _filter, mask, limit, offset): +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 + 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 """ - result = env.client.call(service, method, *parameters, - id=_id, - filter=_filter, - mask=mask, - limit=limit, - offset=offset) - env.fout(formatting.iter_to_table(result)) + + 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': object_filter, + 'mask': mask, + 'limit': limit, + 'offset': offset, + } + + if output_python: + 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