diff -Nru python-softlayer-5.4.4/CHANGELOG.md python-softlayer-5.6.4/CHANGELOG.md --- python-softlayer-5.4.4/CHANGELOG.md 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/CHANGELOG.md 2018-11-16 23:06:56.000000000 +0000 @@ -1,8 +1,84 @@ # Change Log +## [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://console.bluemix.net/docs/vsi/vsi_about_reserved.html#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...master +- 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 diff -Nru python-softlayer-5.4.4/CONTRIBUTING.md python-softlayer-5.6.4/CONTRIBUTING.md --- python-softlayer-5.4.4/CONTRIBUTING.md 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/CONTRIBUTING.md 2018-11-16 23:06:56.000000000 +0000 @@ -12,3 +12,18 @@ * Additional infomration can be found in our [contribution guide](http://softlayer-python.readthedocs.org/en/latest/dev/index.html) +## 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 + + + + diff -Nru python-softlayer-5.4.4/debian/changelog python-softlayer-5.6.4/debian/changelog --- python-softlayer-5.4.4/debian/changelog 2018-04-30 19:35:49.000000000 +0000 +++ python-softlayer-5.6.4/debian/changelog 2019-01-21 18:49:47.000000000 +0000 @@ -1,3 +1,18 @@ +python-softlayer (5.6.4-1) unstable; urgency=medium + + [ Ondřej Nový ] + * d/control: Remove ancient X-Python-Version field + * d/control: Remove ancient X-Python3-Version field + + [ Scott Kitterman ] + * New upstream release + - Update python/python3-prettytable depends to ptable + * Bump standards-version to 4.3.0 without further change + * Run upstream tests as autopkgtest + * Add ptable to pydist overrides + + -- Scott Kitterman Mon, 21 Jan 2019 13:49:47 -0500 + python-softlayer (5.4.4-1) unstable; urgency=medium * New upstream release diff -Nru python-softlayer-5.4.4/debian/control python-softlayer-5.6.4/debian/control --- python-softlayer-5.4.4/debian/control 2018-04-30 19:35:24.000000000 +0000 +++ python-softlayer-5.6.4/debian/control 2019-01-21 18:27:06.000000000 +0000 @@ -10,9 +10,7 @@ python-setuptools, python3-all, python3-setuptools -X-Python-Version: >= 2.6 -X-Python3-Version: >= 3.3 -Standards-Version: 4.1.4 +Standards-Version: 4.3.0 Homepage: https://github.com/softlayer/softlayer-api-python-client Vcs-Git: https://salsa.debian.org/python-team/modules/python-softlayer.git Vcs-Browser: https://salsa.debian.org/python-team/modules/python-softlayer @@ -23,7 +21,7 @@ python-click (>= 5), python-mock, python-nose, - python-prettytable (>= 0.7.0), + python-ptable (>= 0.9.2), python-prompt-toolkit (>= 0.53), python-pygments (>= 2.0.0), python-requests (>= 2.7.0), @@ -44,7 +42,7 @@ python3-click (>= 5), python3-mock, python3-nose, - python3-prettytable (>= 0.7.0), + python3-ptable (>= 0.9.2), python3-prompt-toolkit (>= 0.53), python3-pygments (>= 2.0.0), python3-requests (>= 2.7.0), diff -Nru python-softlayer-5.4.4/debian/patches/0001-Rename-launcher-script-to-avoid-namespace-conflicts.patch python-softlayer-5.6.4/debian/patches/0001-Rename-launcher-script-to-avoid-namespace-conflicts.patch --- python-softlayer-5.4.4/debian/patches/0001-Rename-launcher-script-to-avoid-namespace-conflicts.patch 2018-04-30 19:23:50.000000000 +0000 +++ python-softlayer-5.6.4/debian/patches/0001-Rename-launcher-script-to-avoid-namespace-conflicts.patch 2019-01-18 18:11:14.000000000 +0000 @@ -9,7 +9,7 @@ 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py -index a3f35fd..b70815b 100644 +index a345d27..71753c0 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( diff -Nru python-softlayer-5.4.4/debian/py3dist-overrides python-softlayer-5.6.4/debian/py3dist-overrides --- python-softlayer-5.4.4/debian/py3dist-overrides 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/debian/py3dist-overrides 2019-01-21 18:33:59.000000000 +0000 @@ -0,0 +1 @@ +ptable python3-ptable diff -Nru python-softlayer-5.4.4/debian/pydist-overrides python-softlayer-5.6.4/debian/pydist-overrides --- python-softlayer-5.4.4/debian/pydist-overrides 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/debian/pydist-overrides 2019-01-21 18:33:28.000000000 +0000 @@ -0,0 +1 @@ +ptable python-ptable diff -Nru python-softlayer-5.4.4/debian/tests/control python-softlayer-5.6.4/debian/tests/control --- python-softlayer-5.4.4/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/debian/tests/control 2019-01-20 04:51:58.000000000 +0000 @@ -0,0 +1,3 @@ +Tests: py2 py3 +Depends: @, python, python-mock, python-pytest, python-testtools, python3-all, python3-mock, python3-pytest, python3-testtools +Restrictions: allow-stderr diff -Nru python-softlayer-5.4.4/debian/tests/py2 python-softlayer-5.6.4/debian/tests/py2 --- python-softlayer-5.4.4/debian/tests/py2 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/debian/tests/py2 2019-01-20 04:51:58.000000000 +0000 @@ -0,0 +1,5 @@ +#! /bin/sh + +set -e +python /usr/bin/py.test + diff -Nru python-softlayer-5.4.4/debian/tests/py3 python-softlayer-5.6.4/debian/tests/py3 --- python-softlayer-5.4.4/debian/tests/py3 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/debian/tests/py3 2019-01-20 04:51:58.000000000 +0000 @@ -0,0 +1,5 @@ +#! /bin/sh + +set -e && for i in $(py3versions -sv); do \ + python$i /usr/bin/py.test-3; \ +done diff -Nru python-softlayer-5.4.4/docs/api/client.rst python-softlayer-5.6.4/docs/api/client.rst --- python-softlayer-5.4.4/docs/api/client.rst 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/docs/api/client.rst 2018-11-16 23:06:56.000000000 +0000 @@ -144,6 +144,9 @@ client.call('Account', 'getVirtualGuests', limit=10, offset=0) # Page 1 client.call('Account', 'getVirtualGuests', limit=10, offset=10) # Page 2 + #Automatic Pagination (v5.5.3+) + client.call('Account', 'getVirtualGuests', iter=True) # Page 2 + Here's how to create a new Cloud Compute Instance using `SoftLayer_Virtual_Guest.createObject `_. Be warned, this call actually creates an hourly virtual server so this will @@ -161,6 +164,28 @@ }) +Debugging +------------- +If you ever need to figure out what exact API call the client is making, you can do the following: + +*NOTE* the `print_reproduceable` method produces different output for REST and XML-RPC endpoints. If you are using REST, this will produce a CURL call. IF you are using XML-RPC, it will produce some pure python code you can use outside of the SoftLayer library. + +:: + + # Setup the client as usual + client = SoftLayer.Client() + # Create an instance of the DebugTransport, which logs API calls + debugger = SoftLayer.DebugTransport(client.transport) + # Set that as the default client transport + client.transport = debugger + # Make your API call + client.call('Account', 'getObject') + + # Print out the reproduceable call + for call in client.transport.get_last_calls(): + print(client.transport.print_reproduceable(call)) + + API Reference ------------- diff -Nru python-softlayer-5.4.4/docs/api/managers/vs_capacity.rst python-softlayer-5.6.4/docs/api/managers/vs_capacity.rst --- python-softlayer-5.4.4/docs/api/managers/vs_capacity.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/docs/api/managers/vs_capacity.rst 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,5 @@ +.. _vs_capacity: + +.. automodule:: SoftLayer.managers.vs_capacity + :members: + :inherited-members: diff -Nru python-softlayer-5.4.4/docs/cli/users.rst python-softlayer-5.6.4/docs/cli/users.rst --- python-softlayer-5.4.4/docs/cli/users.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/docs/cli/users.rst 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,93 @@ +.. _cli_user: + +Users +============= +Version 5.6.0 introduces the ability to interact with user accounts from the cli. + +.. _cli_user_create: +user create +----------- +This command will create a user on your account. + +Options +^^^^^^^ +-e, --email TEXT Email address for this user. Required for creation. [required] +-p, --password TEXT Password to set for this user. If no password is provided, user will be sent an email to generate one, which expires in 24 hours. '-p generate' will create a password for you (Requires Python 3.6+). Passwords require 8+ characters, upper and lowercase, a number and a symbol. +-u, --from-user TEXT Base user to use as a template for creating this user. Will default to the user running this command. Information provided in --template supersedes this template. +-t, --template TEXT A json string describing https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/ +-a, --api-key Create an API key for this user. +-h, --help Show this message and exit. + +:: + slcli user create my@email.com -e my@email.com -p generate -a -t '{"firstName": "Test", "lastName": "Testerson"}' + +.. _cli_user_list: + +user list +---------- +This command will list all Active users on the account that your user has access to view. +There is the option to also filter by username + + +.. _cli_user_detail: + +user detail +------------------- +Gives a variety of details about a specific user. can be a user id, or username. Will always print a basic set of information about the user, but there are a few extra flags to pull in more detailed information. + +user detail -p, --permissions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Will list the permissions the user has. To see a list of all possible permissions, or to change a user's permissions, see :ref:`cli_user_permissions` + +user detail -h, --hardware +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Will list the Hardware and Dedicated Hosts the user is able to access. + + +user detail -v, --virtual +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Will list the Virtual Guests the user has access to. + +user detail -l, --logins +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Show login history of this user for the last 30 days. IBMId Users will show logins properly, but may not show failed logins. + +user detail -e, --events +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Shows things that are logged in the Event_Log service. Logins, reboots, reloads, and other such actions will show up here. + +.. _cli_user_permissions: + +user permissions +^^^^^^^^^^^^^^^^^^^^^^^ +Will list off all permission keyNames, along with which are assigned to that specific user. + +.. _cli_user_permissions_edit: + +user edit-permissions +--------------------- +Enable or Disable specific permissions. It is possible to set multiple permissions in one command as well. + +:: + + $ slcli user edit-permissions USERID --enable -p TICKET_EDIT -p TICKET_ADD -p TICKET_SEARCH + +Will enable TICKET_EDIT, TICKET_ADD, and TICKET_SEARCH permissions for the USERID + +.. _cli_user_edit_details: + +user edit-details +----------------- +Edit a User's details + +JSON strings should be enclosed in '' and each item should be enclosed in "\" + +:: + slcli user edit-details testUser -t '{"firstName": "Test", "lastName": "Testerson"}' + +Options +^^^^^^^ +-t, --template TEXT A json string describing `SoftLayer_User_Customer +https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/`_. [required] +-h, --help Show this message and exit. + diff -Nru python-softlayer-5.4.4/docs/cli/vs/reserved_capacity.rst python-softlayer-5.6.4/docs/cli/vs/reserved_capacity.rst --- python-softlayer-5.4.4/docs/cli/vs/reserved_capacity.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/docs/cli/vs/reserved_capacity.rst 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,57 @@ +.. _vs_reserved_capacity_user_docs: + +Working with Reserved Capacity +============================== +There are two main concepts for Reserved Capacity. The `Reserved Capacity Group `_ and the `Reserved Capacity Instance `_ +The Reserved Capacity Group, is a set block of capacity set aside for you at the time of the order. It will contain a set number of Instances which are all the same size. Instances can be ordered like normal VSIs, with the exception that you need to include the reservedCapacityGroupId, and it must be the same size as the group you are ordering the instance in. + +- `About Reserved Capacity `_ +- `Reserved Capacity FAQ `_ + +The SLCLI supports some basic Reserved Capacity Features. + + +.. _cli_vs_capacity_create: + +vs capacity create +------------------ +This command will create a Reserved Capacity Group. + +.. warning:: + + **These groups can not be canceled until their contract expires in 1 or 3 years!** + +:: + + $ slcli vs capacity create --name test-capacity -d dal13 -b 1411193 -c B1_1X2_1_YEAR_TERM -q 10 + +vs cacpacity create_options +--------------------------- +This command will print out the Flavors that can be used to create a Reserved Capacity Group, as well as the backend routers available, as those are needed when creating a new group. + +vs capacity create_guest +------------------------ +This command will create a virtual server (Reserved Capacity Instance) inside of your Reserved Capacity Group. This command works very similar to the `slcli vs create` command. + +:: + + $ slcli vs capacity create-guest --capacity-id 1234 --primary-disk 25 -H ABCD -D test.com -o UBUNTU_LATEST_64 --ipv6 -k test-key --test + +vs capacity detail +------------------ +This command will print out some basic information about the specified Reserved Capacity Group. + +vs capacity list +----------------- +This command will list out all Reserved Capacity Groups. a **#** symbol represents a filled instance, and a **-** symbol respresents an empty instance + +:: + + $ slcli vs capacity list + :............................................................................................................: + : Reserved Capacity : + :......:......................:............:......................:..............:...........................: + : ID : Name : Capacity : Flavor : Location : Created : + :......:......................:............:......................:..............:...........................: + : 1234 : test-capacity : ####------ : B1.1x2 (1 Year Term) : bcr02a.dal13 : 2018-09-24T16:33:09-06:00 : + :......:......................:............:......................:..............:...........................: \ No newline at end of file diff -Nru python-softlayer-5.4.4/docs/cli/vs.rst python-softlayer-5.6.4/docs/cli/vs.rst --- python-softlayer-5.4.4/docs/cli/vs.rst 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/docs/cli/vs.rst 2018-11-16 23:06:56.000000000 +0000 @@ -28,6 +28,8 @@ CPU, operating systems, disk sizes, disk types, datacenters, and so on. Luckily, there's a simple command to show all options: `slcli vs create-options`. +*Some values were ommitted for brevity* + :: $ slcli vs create-options @@ -36,182 +38,16 @@ :................................:.................................................................................: : datacenter : ams01 : : : ams03 : - : : che01 : - : : dal01 : - : : dal05 : - : : dal06 : - : : dal09 : - : : dal10 : - : : dal12 : - : : dal13 : - : : fra02 : - : : hkg02 : - : : hou02 : - : : lon02 : - : : lon04 : - : : lon06 : - : : mel01 : - : : mex01 : - : : mil01 : - : : mon01 : - : : osl01 : - : : par01 : - : : sao01 : - : : sea01 : - : : seo01 : - : : sjc01 : - : : sjc03 : - : : sjc04 : - : : sng01 : - : : syd01 : - : : syd04 : - : : tok02 : - : : tor01 : - : : wdc01 : - : : wdc04 : - : : wdc06 : : : wdc07 : : flavors (balanced) : B1_1X2X25 : : : B1_1X2X25 : : : B1_1X2X100 : - : : B1_1X2X100 : - : : B1_1X4X25 : - : : B1_1X4X25 : - : : B1_1X4X100 : - : : B1_1X4X100 : - : : B1_2X4X25 : - : : B1_2X4X25 : - : : B1_2X4X100 : - : : B1_2X4X100 : - : : B1_2X8X25 : - : : B1_2X8X25 : - : : B1_2X8X100 : - : : B1_2X8X100 : - : : B1_4X8X25 : - : : B1_4X8X25 : - : : B1_4X8X100 : - : : B1_4X8X100 : - : : B1_4X16X25 : - : : B1_4X16X25 : - : : B1_4X16X100 : - : : B1_4X16X100 : - : : B1_8X16X25 : - : : B1_8X16X25 : - : : B1_8X16X100 : - : : B1_8X16X100 : - : : B1_8X32X25 : - : : B1_8X32X25 : - : : B1_8X32X100 : - : : B1_8X32X100 : - : : B1_16X32X25 : - : : B1_16X32X25 : - : : B1_16X32X100 : - : : B1_16X32X100 : - : : B1_16X64X25 : - : : B1_16X64X25 : - : : B1_16X64X100 : - : : B1_16X64X100 : - : : B1_32X64X25 : - : : B1_32X64X25 : - : : B1_32X64X100 : - : : B1_32X64X100 : - : : B1_32X128X25 : - : : B1_32X128X25 : - : : B1_32X128X100 : - : : B1_32X128X100 : - : : B1_48X192X25 : - : : B1_48X192X25 : - : : B1_48X192X100 : - : : B1_48X192X100 : - : flavors (balanced local - hdd) : BL1_1X2X100 : - : : BL1_1X4X100 : - : : BL1_2X4X100 : - : : BL1_2X8X100 : - : : BL1_4X8X100 : - : : BL1_4X16X100 : - : : BL1_8X16X100 : - : : BL1_8X32X100 : - : : BL1_16X32X100 : - : : BL1_16X64X100 : - : : BL1_32X64X100 : - : : BL1_32X128X100 : - : : BL1_56X242X100 : - : flavors (balanced local - ssd) : BL2_1X2X100 : - : : BL2_1X4X100 : - : : BL2_2X4X100 : - : : BL2_2X8X100 : - : : BL2_4X8X100 : - : : BL2_4X16X100 : - : : BL2_8X16X100 : - : : BL2_8X32X100 : - : : BL2_16X32X100 : - : : BL2_16X64X100 : - : : BL2_32X64X100 : - : : BL2_32X128X100 : - : : BL2_56X242X100 : - : flavors (compute) : C1_1X1X25 : - : : C1_1X1X25 : - : : C1_1X1X100 : - : : C1_1X1X100 : - : : C1_2X2X25 : - : : C1_2X2X25 : - : : C1_2X2X100 : - : : C1_2X2X100 : - : : C1_4X4X25 : - : : C1_4X4X25 : - : : C1_4X4X100 : - : : C1_4X4X100 : - : : C1_8X8X25 : - : : C1_8X8X25 : - : : C1_8X8X100 : - : : C1_8X8X100 : - : : C1_16X16X25 : - : : C1_16X16X25 : - : : C1_16X16X100 : - : : C1_16X16X100 : - : : C1_32X32X25 : - : : C1_32X32X25 : - : : C1_32X32X100 : - : : C1_32X32X100 : - : flavors (memory) : M1_1X8X25 : - : : M1_1X8X25 : - : : M1_1X8X100 : - : : M1_1X8X100 : - : : M1_2X16X25 : - : : M1_2X16X25 : - : : M1_2X16X100 : - : : M1_2X16X100 : - : : M1_4X32X25 : - : : M1_4X32X25 : - : : M1_4X32X100 : - : : M1_4X32X100 : - : : M1_8X64X25 : - : : M1_8X64X25 : - : : M1_8X64X100 : - : : M1_8X64X100 : - : : M1_16X128X25 : - : : M1_16X128X25 : - : : M1_16X128X100 : - : : M1_16X128X100 : - : : M1_30X240X25 : - : : M1_30X240X25 : - : : M1_30X240X100 : - : : M1_30X240X100 : - : flavors (GPU) : AC1_8X60X25 : - : : AC1_8X60X100 : - : : AC1_16X120X25 : - : : AC1_16X120X100 : - : : ACL1_8X60X100 : - : : ACL1_16X120X100 : : cpus (standard) : 1,2,4,8,12,16,32,56 : : cpus (dedicated) : 1,2,4,8,16,32,56 : : cpus (dedicated host) : 1,2,4,8,12,16,32,56 : : memory : 1024,2048,4096,6144,8192,12288,16384,32768,49152,65536,131072,247808 : : memory (dedicated host) : 1024,2048,4096,6144,8192,12288,16384,32768,49152,65536,131072,247808 : : os (CENTOS) : CENTOS_5_64 : - : : CENTOS_6_64 : - : : CENTOS_7_64 : - : : CENTOS_LATEST : : : CENTOS_LATEST_64 : : os (CLOUDLINUX) : CLOUDLINUX_5_64 : : : CLOUDLINUX_6_64 : @@ -221,10 +57,6 @@ : : COREOS_LATEST : : : COREOS_LATEST_64 : : os (DEBIAN) : DEBIAN_6_64 : - : : DEBIAN_7_64 : - : : DEBIAN_8_64 : - : : DEBIAN_9_64 : - : : DEBIAN_LATEST : : : DEBIAN_LATEST_64 : : os (OTHERUNIXLINUX) : OTHERUNIXLINUX_1_64 : : : OTHERUNIXLINUX_LATEST : @@ -234,43 +66,11 @@ : : REDHAT_7_64 : : : REDHAT_LATEST : : : REDHAT_LATEST_64 : - : os (UBUNTU) : UBUNTU_12_64 : - : : UBUNTU_14_64 : - : : UBUNTU_16_64 : - : : UBUNTU_LATEST : - : : UBUNTU_LATEST_64 : - : os (VYATTACE) : VYATTACE_6.5_64 : - : : VYATTACE_6.6_64 : - : : VYATTACE_LATEST : - : : VYATTACE_LATEST_64 : - : os (WIN) : WIN_2003-DC-SP2-1_32 : - : : WIN_2003-DC-SP2-1_64 : - : : WIN_2003-ENT-SP2-5_32 : - : : WIN_2003-ENT-SP2-5_64 : - : : WIN_2003-STD-SP2-5_32 : - : : WIN_2003-STD-SP2-5_64 : - : : WIN_2008-STD-R2-SP1_64 : - : : WIN_2008-STD-SP2_32 : - : : WIN_2008-STD-SP2_64 : - : : WIN_2012-STD-R2_64 : - : : WIN_2012-STD_64 : - : : WIN_2016-STD_64 : - : : WIN_LATEST : - : : WIN_LATEST_32 : - : : WIN_LATEST_64 : : san disk(0) : 25,100 : : san disk(2) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : - : san disk(3) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : - : san disk(4) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : - : san disk(5) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : : local disk(0) : 25,100 : : local disk(2) : 25,100,150,200,300 : : local (dedicated host) disk(0) : 25,100 : - : local (dedicated host) disk(2) : 25,100,150,200,300,400 : - : local (dedicated host) disk(3) : 25,100,150,200,300,400 : - : local (dedicated host) disk(4) : 25,100,150,200,300,400 : - : local (dedicated host) disk(5) : 25,100,150,200,300,400 : - : nic : 10,100,1000 : : nic (dedicated host) : 100,1000 : :................................:.................................................................................: @@ -281,7 +81,7 @@ :: - $ slcli vs create --hostname=example --domain=softlayer.com --cpu 2 --memory 1024 -o UBUNTU_14_64 --datacenter=sjc01 --billing=hourly + $ slcli vs create --hostname=example --domain=softlayer.com --cpu 2 --memory 1024 -o DEBIAN_LATEST_64 --datacenter=ams01 --billing=hourly This action will incur charges on your account. Continue? [y/N]: y :.........:......................................: : name : value : @@ -301,7 +101,7 @@ :.........:............:.......................:.......:........:................:..............:....................: : id : datacenter : host : cores : memory : primary_ip : backend_ip : active_transaction : :.........:............:.......................:.......:........:................:..............:....................: - : 1234567 : sjc01 : example.softlayer.com : 2 : 1G : 108.168.200.11 : 10.54.80.200 : Assign Host : + : 1234567 : ams01 : example.softlayer.com : 2 : 1G : 108.168.200.11 : 10.54.80.200 : Assign Host : :.........:............:.......................:.......:........:................:..............:....................: Cool. You may ask, "It's creating... but how do I know when it's done?" Well, @@ -338,12 +138,12 @@ : hostname : example.softlayer.com : : status : Active : : state : Running : - : datacenter : sjc01 : + : datacenter : ams01 : : cores : 2 : : memory : 1G : : public_ip : 108.168.200.11 : : private_ip : 10.54.80.200 : - : os : Ubuntu : + : os : Debian : : private_only : False : : private_cpu : False : : created : 2013-06-13T08:29:44-06:00 : @@ -385,3 +185,12 @@ rescue Reboot into a rescue image. resume Resumes a paused virtual server. upgrade Upgrade a virtual server. + + +Reserved Capacity +----------------- +.. toctree:: + :maxdepth: 2 + + vs/reserved_capacity + diff -Nru python-softlayer-5.4.4/docs/cli.rst python-softlayer-5.6.4/docs/cli.rst --- python-softlayer-5.4.4/docs/cli.rst 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/docs/cli.rst 2018-11-16 23:06:56.000000000 +0000 @@ -15,6 +15,7 @@ cli/ipsec cli/vs cli/ordering + cli/users .. _config_setup: diff -Nru python-softlayer-5.4.4/docs/dev/index.rst python-softlayer-5.6.4/docs/dev/index.rst --- python-softlayer-5.4.4/docs/dev/index.rst 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/docs/dev/index.rst 2018-11-16 23:06:56.000000000 +0000 @@ -87,6 +87,33 @@ py.test tests +Fixtures +~~~~~~~~ + +Testing of this project relies quite heavily on fixtures to simulate API calls. When running the unit tests, we use the FixtureTransport class, which instead of making actual API calls, loads data from `/fixtures/SoftLayer_Service_Name.py` and tries to find a variable that matches the method you are calling. + +When adding new Fixtures you should try to sanitize the data of any account identifiying results, such as account ids, username, and that sort of thing. It is ok to leave the id in place for things like datacenter ids, price ids. + +To Overwrite a fixture, you can use a mock object to do so. Like either of these two methods: + +:: + + # From tests/CLI/modules/vs_capacity_tests.py + from SoftLayer.fixtures import SoftLayer_Product_Package + + def test_create_test(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + + def test_detail_pending(self): + capacity_mock = self.set_mock('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject') + get_object = { + 'name': 'test-capacity', + 'instances': [] + } + capacity_mock.return_value = get_object + + Documentation ------------- The project is documented in @@ -106,6 +133,7 @@ cd docs make html + sphinx-build -b html ./ ./html The primary docs are built at `Read the Docs `_. @@ -121,6 +149,17 @@ tox -e analysis +Autopep8 can fix a lot of the simple flake8 errors about whitespace and indention. + +:: + + autopep8 -r -a -v -i --max-line-length 119 + + + + + + Contributing ------------ diff -Nru python-softlayer-5.4.4/fabfile.py python-softlayer-5.6.4/fabfile.py --- python-softlayer-5.4.4/fabfile.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/fabfile.py 2018-11-16 23:06:56.000000000 +0000 @@ -12,8 +12,8 @@ def upload(): "Upload distribution to PyPi" - local('python setup.py sdist upload') - local('python setup.py bdist_wheel upload') + local('python setup.py sdist bdist_wheel') + local('twine upload dist/*') def clean(): diff -Nru python-softlayer-5.4.4/.gitignore python-softlayer-5.6.4/.gitignore --- python-softlayer-5.4.4/.gitignore 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/.gitignore 2018-11-16 23:06:56.000000000 +0000 @@ -14,3 +14,5 @@ *.egg-info .cache .idea +.pytest_cache/* +slcli diff -Nru python-softlayer-5.4.4/README.rst python-softlayer-5.6.4/README.rst --- python-softlayer-5.4.4/README.rst 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/README.rst 2018-11-16 23:06:56.000000000 +0000 @@ -12,9 +12,12 @@ .. image:: https://coveralls.io/repos/github/softlayer/softlayer-python/badge.svg?branch=master :target: https://coveralls.io/github/softlayer/softlayer-python?branch=master +.. image:: https://build.snapcraft.io/badge/softlayer/softlayer-python.svg + :target: https://build.snapcraft.io/user/softlayer/softlayer-python + 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. @@ -72,9 +75,52 @@ Issues with the Softlayer API itself should be addressed by opening a ticket. + +Examples +-------- + +A curated list of examples on how to use this library can be found at `softlayer.github.io `_ + +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-bock:: python + import SoftLayer + import logging + + class invoices(): + + def __init__(self): + self.client = SoftLayer.Client() + debugger = SoftLayer.DebugTransport(self.client.transport) + self.client.transport = debugger + + def main(self): + mask = "mask[id]" + account = self.client.call('Account', 'getObject', mask=mask); + print("AccountID: %s" % account['id']) + + def debug(self): + for call in self.client.transport.get_last_calls(): + print(self.client.transport.print_reproduceable(call)) + + if __name__ == "__main__": + main = example() + main.main() + main.debug() + System Requirements ------------------- -* Python 2.7, 3.3, 3.4, 3.5 or 3.6. +* Python 2.7, 3.3, 3.4, 3.5, 3.6, or 3.7. * A valid SoftLayer API username and key. * A connection to SoftLayer's private network is required to use our private network API endpoints. @@ -83,7 +129,7 @@ --------------- * six >= 1.7.0 * prettytable >= 0.7.0 -* click >= 5 +* click >= 5, < 7 * requests >= 2.18.4 * prompt_toolkit >= 0.53 * pygments >= 2.0.0 diff -Nru python-softlayer-5.4.4/setup.cfg python-softlayer-5.6.4/setup.cfg --- python-softlayer-5.4.4/setup.cfg 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/setup.cfg 2018-11-16 23:06:56.000000000 +0000 @@ -1,5 +1,8 @@ [tool:pytest] python_files = *_tests.py +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning [wheel] universal=1 diff -Nru python-softlayer-5.4.4/setup.py python-softlayer-5.6.4/setup.py --- python-softlayer-5.4.4/setup.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/setup.py 2018-11-16 23:06:56.000000000 +0000 @@ -14,7 +14,7 @@ setup( name='SoftLayer', - version='5.4.4', + version='5.6.4', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.', @@ -31,14 +31,14 @@ }, install_requires=[ 'six >= 1.7.0', - 'prettytable >= 0.7.0', - 'click >= 5', - 'requests >= 2.18.4', + 'ptable >= 0.9.2', + 'click >= 7', + 'requests >= 2.20.0', 'prompt_toolkit >= 0.53', 'pygments >= 2.0.0', - 'urllib3 >= 1.22' + 'urllib3 >= 1.24' ], - keywords=['softlayer', 'cloud'], + keywords=['softlayer', 'cloud', 'slcli'], classifiers=[ 'Environment :: Console', 'Environment :: Web Environment', diff -Nru python-softlayer-5.4.4/snap/README.md python-softlayer-5.6.4/snap/README.md --- python-softlayer-5.4.4/snap/README.md 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/snap/README.md 2018-11-16 23:06:56.000000000 +0000 @@ -10,3 +10,8 @@ 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 -Nru python-softlayer-5.4.4/snap/snapcraft.yaml python-softlayer-5.6.4/snap/snapcraft.yaml --- python-softlayer-5.4.4/snap/snapcraft.yaml 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/snap/snapcraft.yaml 2018-11-16 23:06:56.000000000 +0000 @@ -1,5 +1,5 @@ name: slcli # check to see if it's available -version: '5.4.3.0+git' # check versioning +version: '5.6.4+git' # check versioning summary: Python based SoftLayer API Tool. # 79 char long summary description: | A command-line interface is also included and can be used to manage various SoftLayer products and services. @@ -20,7 +20,8 @@ my-part: source: https://github.com/softlayer/softlayer-python source-type: git - plugin: python3 + plugin: python + python-version: python3 build-packages: - python3 diff -Nru python-softlayer-5.4.4/SoftLayer/API.py python-softlayer-5.6.4/SoftLayer/API.py --- python-softlayer-5.4.4/SoftLayer/API.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/API.py 2018-11-16 23:06:56.000000000 +0000 @@ -214,7 +214,9 @@ """ 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: @@ -267,55 +269,49 @@ :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) - - # It looks like we ran out results - if not results: - break + result_count = 0 + keep_looping = True + + while keep_looping: + # Get the next results + results = self.call(service, method, offset=offset, limit=limit, *args, **kwargs) # Apparently this method doesn't return a list. # Why are you even iterating over this? - if not isinstance(results, list): - yield results - break + if not isinstance(results, transports.SoftLayerListResult): + if isinstance(results, list): + # Close enough, this makes testing a lot easier + results = transports.SoftLayerListResult(results, len(results)) + else: + yield results + return for item in results: yield item result_count += 1 - offset += chunk + # Got less results than requested, we are at the end + if len(results) < limit: + keep_looping = False + # Got all the needed items + if result_count >= results.total_count: + keep_looping = False - if len(results) < chunk: - break + offset += limit def __repr__(self): return "Client(transport=%r, auth=%r)" % (self.transport, self.auth) @@ -333,6 +329,7 @@ :param name str: The service name """ + def __init__(self, client, name): self.client = client self.name = name diff -Nru python-softlayer-5.4.4/SoftLayer/auth.py python-softlayer-5.6.4/SoftLayer/auth.py --- python-softlayer-5.4.4/SoftLayer/auth.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/auth.py 2018-11-16 23:06:56.000000000 +0000 @@ -42,6 +42,7 @@ :param auth_token str: a user's auth token, attained through User_Customer::getPortalLoginToken """ + def __init__(self, user_id, auth_token): self.user_id = user_id self.auth_token = auth_token @@ -65,6 +66,7 @@ :param username str: a user's username :param api_key str: a user's API key """ + def __init__(self, username, api_key): self.username = username self.api_key = api_key @@ -87,6 +89,7 @@ :param username str: a user's username :param api_key str: a user's API key """ + def __init__(self, username, api_key): self.username = username self.api_key = api_key diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/block/detail.py python-softlayer-5.6.4/SoftLayer/CLI/block/detail.py --- python-softlayer-5.4.4/SoftLayer/CLI/block/detail.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/block/detail.py 2018-11-16 23:06:56.000000000 +0000 @@ -29,7 +29,7 @@ table.add_row(['LUN Id', "%s" % block_volume['lunId']]) if block_volume.get('provisionedIops'): - table.add_row(['IOPs', int(block_volume['provisionedIops'])]) + table.add_row(['IOPs', float(block_volume['provisionedIops'])]) if block_volume.get('storageTierLevel'): table.add_row([ diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/block/list.py python-softlayer-5.6.4/SoftLayer/CLI/block/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/block/list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/block/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -23,6 +23,7 @@ mask="storageType.keyName"), column_helper.Column('capacity_gb', ('capacityGb',), mask="capacityGb"), column_helper.Column('bytes_used', ('bytesUsed',), mask="bytesUsed"), + column_helper.Column('iops', ('iops',), mask="iops"), column_helper.Column('ip_addr', ('serviceResourceBackendIpAddress',), mask="serviceResourceBackendIpAddress"), column_helper.Column('lunId', ('lunId',), mask="lunId"), @@ -42,6 +43,7 @@ 'storage_type', 'capacity_gb', 'bytes_used', + 'iops', 'ip_addr', 'lunId', 'active_transactions', diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/block/order.py python-softlayer-5.6.4/SoftLayer/CLI/block/order.py --- python-softlayer-5.4.4/SoftLayer/CLI/block/order.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/block/order.py 2018-11-16 23:06:56.000000000 +0000 @@ -17,17 +17,14 @@ required=True) @click.option('--size', type=int, - help='Size of block storage volume in GB. Permitted Sizes:\n' - '20, 40, 80, 100, 250, 500, 1000, 2000, 4000, 8000, 12000', + help='Size of block storage volume in GB.', required=True) @click.option('--iops', type=int, - help='Performance Storage IOPs,' - ' between 100 and 6000 in multiples of 100' - ' [required for storage-type performance]') + help="""Performance Storage IOPs. Options vary based on storage size. +[required for storage-type performance]""") @click.option('--tier', - help='Endurance Storage Tier (IOP per GB)' - ' [required for storage-type endurance]', + help='Endurance Storage Tier (IOP per GB) [required for storage-type endurance]', type=click.Choice(['0.25', '2', '4', '10'])) @click.option('--os-type', help='Operating System', @@ -49,8 +46,8 @@ 'space along with endurance block storage; specifies ' 'the size (in GB) of snapshot space to order') @click.option('--service-offering', - help='The service offering package to use for placing ' - 'the order [optional, default is \'storage_as_a_service\']', + help="""The service offering package to use for placing the order. +[optional, default is \'storage_as_a_service\']. enterprise and performance are depreciated""", default='storage_as_a_service', type=click.Choice([ 'storage_as_a_service', @@ -63,7 +60,11 @@ @environment.pass_env def cli(env, storage_type, size, iops, tier, os_type, location, snapshot_size, service_offering, billing): - """Order a block storage volume.""" + """Order a block storage volume. + + Valid size and iops options can be found here: + https://console.bluemix.net/docs/infrastructure/BlockStorage/index.html#provisioning + """ block_manager = SoftLayer.BlockStorageManager(env.client) storage_type = storage_type.lower() @@ -71,26 +72,21 @@ if billing.lower() == "hourly": hourly_billing_flag = True - if hourly_billing_flag and service_offering != 'storage_as_a_service': - raise exceptions.CLIAbort( - 'Hourly billing is only available for the storage_as_a_service ' - 'service offering' - ) + if service_offering != 'storage_as_a_service': + click.secho('{} is a legacy storage offering'.format(service_offering), fg='red') + if hourly_billing_flag: + raise exceptions.CLIAbort( + 'Hourly billing is only available for the storage_as_a_service service offering' + ) if storage_type == 'performance': if iops is None: - raise exceptions.CLIAbort( - 'Option --iops required with Performance') - - if iops % 100 != 0: - raise exceptions.CLIAbort( - 'Option --iops must be a multiple of 100' - ) + raise exceptions.CLIAbort('Option --iops required with Performance') if service_offering == 'performance' and snapshot_size is not None: raise exceptions.CLIAbort( - '--snapshot-size is not available for performance volumes ' - 'ordered with the \'performance\' service offering option' + '--snapshot-size is not available for performance service offerings. ' + 'Use --service-offering storage_as_a_service' ) try: @@ -110,8 +106,7 @@ if storage_type == 'endurance': if tier is None: raise exceptions.CLIAbort( - 'Option --tier required with Endurance in IOPS/GB ' - '[0.25,2,4,10]' + 'Option --tier required with Endurance in IOPS/GB [0.25,2,4,10]' ) try: @@ -134,5 +129,4 @@ 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.") + click.echo("Order could not be placed! Please verify your options and try again.") diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/columns.py python-softlayer-5.6.4/SoftLayer/CLI/columns.py --- python-softlayer-5.4.4/SoftLayer/CLI/columns.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/columns.py 2018-11-16 23:06:56.000000000 +0000 @@ -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 @@ class ColumnFormatter(object): """Maps each column using a function""" + def __init__(self): self.columns = [] self.column_funcs = [] @@ -55,7 +57,7 @@ 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 -Nru python-softlayer-5.4.4/SoftLayer/CLI/core.py python-softlayer-5.6.4/SoftLayer/CLI/core.py --- python-softlayer-5.4.4/SoftLayer/CLI/core.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/core.py 2018-11-16 23:06:56.000000000 +0000 @@ -75,7 +75,8 @@ use: 'slcli setup'""", cls=CommandLoader, context_settings={'help_option_names': ['-h', '--help'], - 'auto_envvar_prefix': 'SLCLI'}) + 'auto_envvar_prefix': 'SLCLI', + 'max_content_width': 999}) @click.option('--format', default=DEFAULT_FORMAT, show_default=True, @@ -114,24 +115,29 @@ **kwargs): """Main click CLI entry-point.""" - logger = logging.getLogger() - logger.addHandler(logging.StreamHandler()) - logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG)) - # Populate environement with client and set it as the context object env.skip_confirmations = really env.config_file = config env.format = format env.ensure_client(config_file=config, is_demo=demo, proxy=proxy) - env.vars['_start'] = time.time() + logger = logging.getLogger() + + if demo is False: + logger.addHandler(logging.StreamHandler()) + else: + # This section is for running CLI tests. + logging.getLogger("urllib3").setLevel(logging.WARNING) + logger.addHandler(logging.NullHandler()) + + logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG)) env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport) env.client.transport = env.vars['_timings'] @cli.resultcallback() @environment.pass_env -def output_diagnostics(env, verbose=0, **kwargs): +def output_diagnostics(env, result, verbose=0, **kwargs): """Output diagnostic information.""" if verbose > 0: diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/dedicatedhost/cancel_guests.py python-softlayer-5.6.4/SoftLayer/CLI/dedicatedhost/cancel_guests.py --- python-softlayer-5.4.4/SoftLayer/CLI/dedicatedhost/cancel_guests.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/dedicatedhost/cancel_guests.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,43 @@ +"""Cancel a dedicated host.""" +# :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 +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Cancel all virtual guests of the dedicated host immediately. + + Use the 'slcli vs cancel' command to cancel an specific guest + """ + + dh_mgr = SoftLayer.DedicatedHostManager(env.client) + + host_id = helpers.resolve_id(dh_mgr.resolve_ids, identifier, 'dedicated host') + + if not (env.skip_confirmations or formatting.no_going_back(host_id)): + raise exceptions.CLIAbort('Aborted') + + table = formatting.Table(['id', 'server name', 'status']) + + result = dh_mgr.cancel_guests(host_id) + + if result: + for status in result: + table.add_row([ + status['id'], + status['fqdn'], + status['status'] + ]) + + env.fout(table) + else: + click.secho('There is not any guest into the dedicated host %s' % host_id, fg='red') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/dedicatedhost/cancel.py python-softlayer-5.6.4/SoftLayer/CLI/dedicatedhost/cancel.py --- python-softlayer-5.4.4/SoftLayer/CLI/dedicatedhost/cancel.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/dedicatedhost/cancel.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,28 @@ +"""Cancel a dedicated host.""" +# :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 +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Cancel a dedicated host server immediately""" + + mgr = SoftLayer.DedicatedHostManager(env.client) + + host_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'dedicated host') + + if not (env.skip_confirmations or formatting.no_going_back(host_id)): + raise exceptions.CLIAbort('Aborted') + + mgr.cancel_host(host_id) + + click.secho('Dedicated Host %s was cancelled' % host_id, fg='green') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/dedicatedhost/list_guests.py python-softlayer-5.6.4/SoftLayer/CLI/dedicatedhost/list_guests.py --- python-softlayer-5.4.4/SoftLayer/CLI/dedicatedhost/list_guests.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/dedicatedhost/list_guests.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,76 @@ +"""List guests which are in a dedicated host server.""" +# :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('guid', ('globalIdentifier',)), + column_helper.Column('cpu', ('maxCpu',)), + column_helper.Column('memory', ('maxMemory',)), + column_helper.Column('datacenter', ('datacenter', 'name')), + column_helper.Column('primary_ip', ('primaryIpAddress',)), + column_helper.Column('backend_ip', ('primaryBackendIpAddress',)), + column_helper.Column( + 'created_by', + ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), + column_helper.Column('power_state', ('powerState', 'name')), + column_helper.Column( + 'tags', + lambda server: formatting.tags(server.get('tagReferences')), + mask="tagReferences.tag.name"), +] + +DEFAULT_COLUMNS = [ + 'id', + 'hostname', + 'domain', + 'primary_ip', + 'backend_ip', + 'power_state' +] + + +@click.command() +@click.argument('identifier') +@click.option('--cpu', '-c', help='Number of CPU cores', type=click.INT) +@click.option('--domain', '-D', help='Domain portion of the FQDN') +@click.option('--hostname', '-H', help='Host portion of the FQDN') +@click.option('--memory', '-m', help='Memory in mebibytes', type=click.INT) +@helpers.multi_option('--tag', help='Filter by tags') +@click.option('--sortby', + help='Column to sort by', + default='hostname', + show_default=True) +@click.option('--columns', + callback=column_helper.get_formatter(COLUMNS), + help='Columns to display. [options: %s]' + % ', '.join(column.name for column in COLUMNS), + default=','.join(DEFAULT_COLUMNS), + show_default=True) +@environment.pass_env +def cli(env, identifier, sortby, cpu, domain, hostname, memory, tag, columns): + """List guests which are in a dedicated host server.""" + + mgr = SoftLayer.DedicatedHostManager(env.client) + guests = mgr.list_guests(host_id=identifier, + cpus=cpu, + hostname=hostname, + domain=domain, + memory=memory, + tags=tag, + mask=columns.mask()) + + table = formatting.Table(columns.columns) + table.sortby = sortby + + for guest in guests: + table.add_row([value or formatting.blank() + for value in columns.row(guest)]) + + env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/dns/record_add.py python-softlayer-5.6.4/SoftLayer/CLI/dns/record_add.py --- python-softlayer-5.4.4/SoftLayer/CLI/dns/record_add.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/dns/record_add.py 2018-11-16 23:06:56.000000000 +0000 @@ -5,24 +5,86 @@ import SoftLayer from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions from SoftLayer.CLI import helpers # pylint: disable=redefined-builtin @click.command() -@click.argument('zone') @click.argument('record') -@click.argument('type') +@click.argument('record_type') @click.argument('data') +@click.option('--zone', + help="Zone name or identifier that the resource record will be associated with.\n" + "Required for all record types except PTR") @click.option('--ttl', - type=click.INT, - default=7200, + default=900, show_default=True, help='TTL value in seconds, such as 86400') +@click.option('--priority', + default=10, + show_default=True, + help='The priority of the target host. (MX or SRV type only)') +@click.option('--protocol', + type=click.Choice(['tcp', 'udp', 'tls']), + default='tcp', + show_default=True, + help='The protocol of the service, usually either TCP or UDP. (SRV type only)') +@click.option('--port', + type=click.INT, + help='The TCP/UDP/TLS port on which the service is to be found. (SRV type only)') +@click.option('--service', + help='The symbolic name of the desired service. (SRV type only)') +@click.option('--weight', + default=5, + show_default=True, + help='Relative weight for records with same priority. (SRV type only)') @environment.pass_env -def cli(env, zone, record, type, data, ttl): - """Add resource record.""" +def cli(env, record, record_type, data, zone, ttl, priority, protocol, port, service, weight): + """Add resource record. + + Each resource record contains a RECORD and DATA property, defining a resource's name and it's target data. + Domains contain multiple types of resource records so it can take one of the following values: A, AAAA, CNAME, + MX, SPF, SRV, and PTR. + + About reverse records (PTR), the RECORD value must to be the public Ip Address of device you would like to manage + reverse DNS. + + slcli dns record-add 10.10.8.21 PTR myhost.com --ttl=900 + + Examples: + + slcli dns record-add myhost.com A 192.168.1.10 --zone=foobar.com --ttl=900 + + slcli dns record-add myhost.com AAAA 2001:DB8::1 --zone=foobar.com + + slcli dns record-add 192.168.1.2 MX 192.168.1.10 --zone=foobar.com --priority=11 --ttl=1800 + + slcli dns record-add myhost.com TXT "txt-verification=rXOxyZounZs87oacJSKvbUSIQ" --zone=2223334 + + slcli dns record-add myhost.com SPF "v=spf1 include:_spf.google.com ~all" --zone=2223334 + + slcli dns record-add myhost.com SRV 192.168.1.10 --zone=2223334 --service=foobar --port=80 --protocol=TCP + + """ manager = SoftLayer.DNSManager(env.client) - zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') - manager.create_record(zone_id, record, type, data, ttl=ttl) + record_type = record_type.upper() + + if zone and record_type != 'PTR': + zone_id = helpers.resolve_id(manager.resolve_ids, zone, name='zone') + + if record_type == 'MX': + manager.create_record_mx(zone_id, record, data, ttl=ttl, priority=priority) + elif record_type == 'SRV': + manager.create_record_srv(zone_id, record, data, protocol, port, service, + ttl=ttl, priority=priority, weight=weight) + else: + manager.create_record(zone_id, record, record_type, data, ttl=ttl) + + elif record_type == 'PTR': + manager.create_record_ptr(record, data, ttl=ttl) + else: + raise exceptions.CLIAbort("%s isn't a valid record type or zone is missing" % record_type) + + click.secho("%s record added successfully" % record_type, fg='green') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/exceptions.py python-softlayer-5.6.4/SoftLayer/CLI/exceptions.py --- python-softlayer-5.4.4/SoftLayer/CLI/exceptions.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/exceptions.py 2018-11-16 23:06:56.000000000 +0000 @@ -10,6 +10,7 @@ # pylint: disable=keyword-arg-before-vararg class CLIHalt(SystemExit): """Smoothly halt the execution of the command. No error.""" + def __init__(self, code=0, *args): super(CLIHalt, self).__init__(*args) self.code = code @@ -23,6 +24,7 @@ class CLIAbort(CLIHalt): """Halt the execution of the command. Gives an exit code of 2.""" + def __init__(self, msg, *args): super(CLIAbort, self).__init__(code=2, *args) self.message = msg @@ -30,6 +32,7 @@ class ArgumentError(CLIAbort): """Halt the execution of the command because of invalid arguments.""" + def __init__(self, msg, *args): super(ArgumentError, self).__init__(msg, *args) self.message = "Argument Error: %s" % msg diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/file/order.py python-softlayer-5.6.4/SoftLayer/CLI/file/order.py --- python-softlayer-5.4.4/SoftLayer/CLI/file/order.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/file/order.py 2018-11-16 23:06:56.000000000 +0000 @@ -21,12 +21,10 @@ required=True) @click.option('--iops', type=int, - help='Performance Storage IOPs,' - ' between 100 and 6000 in multiples of 100' - ' [required for storage-type performance]') + help="""Performance Storage IOPs. Options vary based on storage size. +[required for storage-type performance]""") @click.option('--tier', - help='Endurance Storage Tier (IOP per GB)' - ' [required for storage-type endurance]', + help='Endurance Storage Tier (IOP per GB) [required for storage-type endurance]', type=click.Choice(['0.25', '2', '4', '10'])) @click.option('--location', help='Datacenter short name (e.g.: dal09)', @@ -37,8 +35,8 @@ 'space along with endurance file storage; specifies ' 'the size (in GB) of snapshot space to order') @click.option('--service-offering', - help='The service offering package to use for placing ' - 'the order [optional, default is \'storage_as_a_service\']', + help="""The service offering package to use for placing the order. +[optional, default is \'storage_as_a_service\']. enterprise and performance are depreciated""", default='storage_as_a_service', type=click.Choice([ 'storage_as_a_service', @@ -51,7 +49,11 @@ @environment.pass_env def cli(env, storage_type, size, iops, tier, location, snapshot_size, service_offering, billing): - """Order a file storage volume.""" + """Order a file storage volume. + + Valid size and iops options can be found here: + https://console.bluemix.net/docs/infrastructure/FileStorage/index.html#provisioning + """ file_manager = SoftLayer.FileStorageManager(env.client) storage_type = storage_type.lower() @@ -59,26 +61,21 @@ if billing.lower() == "hourly": hourly_billing_flag = True - if hourly_billing_flag and service_offering != 'storage_as_a_service': - raise exceptions.CLIAbort( - 'Hourly billing is only available for the storage_as_a_service ' - 'service offering' - ) + if service_offering != 'storage_as_a_service': + click.secho('{} is a legacy storage offering'.format(service_offering), fg='red') + if hourly_billing_flag: + raise exceptions.CLIAbort( + 'Hourly billing is only available for the storage_as_a_service service offering' + ) if storage_type == 'performance': if iops is None: - raise exceptions.CLIAbort( - 'Option --iops required with Performance') - - if iops % 100 != 0: - raise exceptions.CLIAbort( - 'Option --iops must be a multiple of 100' - ) + raise exceptions.CLIAbort('Option --iops required with Performance') if service_offering == 'performance' and snapshot_size is not None: raise exceptions.CLIAbort( - '--snapshot-size is not available for performance volumes ' - 'ordered with the \'performance\' service offering option' + '--snapshot-size is not available for performance service offerings. ' + 'Use --service-offering storage_as_a_service' ) try: @@ -97,8 +94,7 @@ if storage_type == 'endurance': if tier is None: raise exceptions.CLIAbort( - 'Option --tier required with Endurance in IOPS/GB ' - '[0.25,2,4,10]' + 'Option --tier required with Endurance in IOPS/GB [0.25,2,4,10]' ) try: @@ -120,5 +116,4 @@ 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.") + click.echo("Order could not be placed! Please verify your options and try again.") diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/file/snapshot/schedule_list.py python-softlayer-5.6.4/SoftLayer/CLI/file/snapshot/schedule_list.py --- python-softlayer-5.4.4/SoftLayer/CLI/file/snapshot/schedule_list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/file/snapshot/schedule_list.py 2018-11-16 23:06:56.000000000 +0000 @@ -63,7 +63,7 @@ file_schedule_type, replication, schedule.get('createDate', '') - ] + ] table_row.extend(schedule_properties) table.add_row(table_row) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/formatting.py python-softlayer-5.6.4/SoftLayer/CLI/formatting.py --- python-softlayer-5.4.4/SoftLayer/CLI/formatting.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/formatting.py 2018-11-16 23:06:56.000000000 +0000 @@ -1,10 +1,8 @@ """ SoftLayer.formatting ~~~~~~~~~~~~~~~~~~~~ - Provider classes and helper functions to display output onto a - command-line. + Provider classes and helper functions to display output onto a command-line. - :license: MIT, see LICENSE for more details. """ # pylint: disable=E0202, consider-merging-isinstance, arguments-differ, keyword-arg-before-vararg import collections @@ -12,7 +10,12 @@ import os import click -import prettytable + +# If both PTable and prettytable are installed, its impossible to use the new version +try: + from prettytable import prettytable +except ImportError: + import prettytable from SoftLayer.CLI import exceptions from SoftLayer import utils @@ -229,6 +232,7 @@ :param separator str: string to use as a default separator """ + def __init__(self, separator=os.linesep, *args, **kwargs): self.separator = separator super(SequentialOutput, self).__init__(*args, **kwargs) @@ -243,6 +247,7 @@ class CLIJSONEncoder(json.JSONEncoder): """A JSON encoder which is able to use a .to_python() method on objects.""" + def default(self, obj): """Encode object if it implements to_python().""" if hasattr(obj, 'to_python'): @@ -255,7 +260,8 @@ :param list columns: a list of column names """ - def __init__(self, columns): + + def __init__(self, columns, title=None): duplicated_cols = [col for col, count in collections.Counter(columns).items() if count > 1] @@ -267,6 +273,7 @@ self.rows = [] self.align = {} self.sortby = None + self.title = title def add_row(self, row): """Add a row to the table. @@ -287,6 +294,7 @@ def prettytable(self): """Returns a new prettytable instance.""" table = prettytable.PrettyTable(self.columns) + if self.sortby: if self.sortby in self.columns: table.sortby = self.sortby @@ -296,6 +304,8 @@ for a_col, alignment in self.align.items(): table.align[a_col] = alignment + if self.title: + table.title = self.title # Adding rows for row in self.rows: table.add_row(row) @@ -304,6 +314,7 @@ class KeyValueTable(Table): """A table that is oriented towards key-value pairs.""" + def to_python(self): """Decode this KeyValueTable object to standard Python types.""" mapping = {} @@ -318,6 +329,7 @@ :param original: raw (machine-readable) value :param string formatted: human-readable value """ + def __init__(self, original, formatted=None): self.original = original if formatted is not None: diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/hardware/credentials.py python-softlayer-5.6.4/SoftLayer/CLI/hardware/credentials.py --- python-softlayer-5.4.4/SoftLayer/CLI/hardware/credentials.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/hardware/credentials.py 2018-11-16 23:06:56.000000000 +0000 @@ -7,6 +7,7 @@ from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +from SoftLayer import exceptions @click.command() @@ -22,6 +23,9 @@ instance = manager.get_hardware(hardware_id) table = formatting.Table(['username', 'password']) - for item in instance['operatingSystem']['passwords']: - table.add_row([item['username'], item['password']]) + for item in instance['softwareComponents']: + if 'passwords' not in item: + raise exceptions.SoftLayerError("No passwords found in softwareComponents") + for credentials in item['passwords']: + table.add_row([credentials.get('username', 'None'), credentials.get('password', 'None')]) env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/hardware/list.py python-softlayer-5.6.4/SoftLayer/CLI/hardware/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/hardware/list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/hardware/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -55,8 +55,12 @@ help='Columns to display. [options: %s]' % ', '.join(column.name for column in COLUMNS), default=','.join(DEFAULT_COLUMNS), show_default=True) +@click.option('--limit', '-l', + help='How many results to get in one api call, default is 100', + default=100, + show_default=True) @environment.pass_env -def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, columns): +def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, columns, limit): """List hardware servers.""" manager = SoftLayer.HardwareManager(env.client) @@ -67,7 +71,8 @@ datacenter=datacenter, nic_speed=network, tags=tag, - mask="mask(SoftLayer_Hardware_Server)[%s]" % columns.mask()) + mask="mask(SoftLayer_Hardware_Server)[%s]" % columns.mask(), + limit=limit) table = formatting.Table(columns.columns) table.sortby = sortby diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/image/export.py python-softlayer-5.6.4/SoftLayer/CLI/image/export.py --- python-softlayer-5.4.4/SoftLayer/CLI/image/export.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/image/export.py 2018-11-16 23:06:56.000000000 +0000 @@ -12,17 +12,25 @@ @click.command() @click.argument('identifier') @click.argument('uri') +@click.option('--ibm-api-key', + default=None, + help="The IBM Cloud API Key with access to IBM Cloud Object " + "Storage instance. For help creating this key see " + "https://console.bluemix.net/docs/services/cloud-object-" + "storage/iam/users-serviceids.html#serviceidapikeys") @environment.pass_env -def cli(env, identifier, uri): +def cli(env, identifier, uri, ibm_api_key): """Export an image to object storage. The URI for an object storage object (.vhd/.iso file) of the format: swift://@// + or cos://// if using IBM Cloud + Object Storage """ image_mgr = SoftLayer.ImageManager(env.client) image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') - result = image_mgr.export_image_to_uri(image_id, uri) + result = image_mgr.export_image_to_uri(image_id, uri, ibm_api_key) if not result: raise exceptions.CLIAbort("Failed to export Image") diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/image/import.py python-softlayer-5.6.4/SoftLayer/CLI/image/import.py --- python-softlayer-5.4.4/SoftLayer/CLI/image/import.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/image/import.py 2018-11-16 23:06:56.000000000 +0000 @@ -16,15 +16,44 @@ default="", help="The note to be applied to the imported template") @click.option('--os-code', - default="", help="The referenceCode of the operating system software" - " description for the imported VHD") + " description for the imported VHD, ISO, or RAW image") +@click.option('--ibm-api-key', + default=None, + help="The IBM Cloud API Key with access to IBM Cloud Object " + "Storage instance and IBM KeyProtect instance. For help " + "creating this key see https://console.bluemix.net/docs/" + "services/cloud-object-storage/iam/users-serviceids.html" + "#serviceidapikeys") +@click.option('--root-key-id', + default=None, + help="ID of the root key in Key Protect") +@click.option('--wrapped-dek', + default=None, + help="Wrapped Data Encryption Key provided by IBM KeyProtect. " + "For more info see https://console.bluemix.net/docs/" + "services/key-protect/wrap-keys.html#wrap-keys") +@click.option('--kp-id', + default=None, + help="ID of the IBM Key Protect Instance") +@click.option('--cloud-init', + is_flag=True, + help="Specifies if image is cloud-init") +@click.option('--byol', + is_flag=True, + help="Specifies if image is bring your own license") +@click.option('--is-encrypted', + is_flag=True, + help="Specifies if image is encrypted") @environment.pass_env -def cli(env, name, note, os_code, uri): +def cli(env, name, note, os_code, uri, ibm_api_key, root_key_id, wrapped_dek, + kp_id, cloud_init, byol, is_encrypted): """Import an image. The URI for an object storage object (.vhd/.iso file) of the format: swift://@// + or cos://// if using IBM Cloud + Object Storage """ image_mgr = SoftLayer.ImageManager(env.client) @@ -33,6 +62,13 @@ note=note, os_code=os_code, uri=uri, + ibm_api_key=ibm_api_key, + root_key_id=root_key_id, + wrapped_dek=wrapped_dek, + kp_id=kp_id, + cloud_init=cloud_init, + byol=byol, + is_encrypted=is_encrypted ) if not result: diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/nas/credentials.py python-softlayer-5.6.4/SoftLayer/CLI/nas/credentials.py --- python-softlayer-5.4.4/SoftLayer/CLI/nas/credentials.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/nas/credentials.py 2018-11-16 23:06:56.000000000 +0000 @@ -17,6 +17,6 @@ nw_mgr = SoftLayer.NetworkManager(env.client) result = nw_mgr.get_nas_credentials(identifier) table = formatting.Table(['username', 'password']) - table.add_row([result['username'], - result['password']]) + table.add_row([result.get('username', 'None'), + result.get('password', 'None')]) env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/order/__init__.py python-softlayer-5.6.4/SoftLayer/CLI/order/__init__.py --- python-softlayer-5.4.4/SoftLayer/CLI/order/__init__.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/order/__init__.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,2 @@ +"""View and order from the catalog.""" +# :license: MIT, see LICENSE for more details. diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/order/package_list.py python-softlayer-5.6.4/SoftLayer/CLI/order/package_list.py --- python-softlayer-5.4.4/SoftLayer/CLI/order/package_list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/order/package_list.py 2018-11-16 23:06:56.000000000 +0000 @@ -7,7 +7,8 @@ from SoftLayer.CLI import formatting from SoftLayer.managers import ordering -COLUMNS = ['name', +COLUMNS = ['id', + 'name', 'keyName', 'type'] @@ -51,6 +52,7 @@ for package in packages: table.add_row([ + package['id'], package['name'], package['keyName'], package['type']['keyName'] diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/order/place.py python-softlayer-5.6.4/SoftLayer/CLI/order/place.py --- python-softlayer-5.4.4/SoftLayer/CLI/order/place.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/order/place.py 2018-11-16 23:06:56.000000000 +0000 @@ -43,7 +43,7 @@ can then be converted to be made programmatically by calling SoftLayer.OrderingManager.place_order() with the same keynames. - Packages for ordering can be retrived from `slcli order package-list` + Packages for ordering can be retrieved from `slcli order package-list` Presets for ordering can be retrieved from `slcli order preset-list` (not all packages have presets) @@ -88,7 +88,7 @@ if verify: table = formatting.Table(COLUMNS) order_to_place = manager.verify_order(*args, **kwargs) - for price in order_to_place['prices']: + for price in order_to_place['orderContainers'][0]['prices']: cost_key = 'hourlyRecurringFee' if billing == 'hourly' else 'recurringFee' table.add_row([ price['item']['keyName'], diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/order/place_quote.py python-softlayer-5.6.4/SoftLayer/CLI/order/place_quote.py --- python-softlayer-5.4.4/SoftLayer/CLI/order/place_quote.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/order/place_quote.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,91 @@ +"""Place quote""" +# :license: MIT, see LICENSE for more details. + +import json + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering + + +@click.command() +@click.argument('package_keyname') +@click.argument('location') +@click.option('--preset', + help="The order preset (if required by the package)") +@click.option('--name', + help="A custom name to be assigned to the quote (optional)") +@click.option('--send-email', + is_flag=True, + help="The quote will be sent to the email address associated.") +@click.option('--complex-type', help=("The complex type of the order. This typically begins" + " with 'SoftLayer_Container_Product_Order_'.")) +@click.option('--extras', + help="JSON string denoting extra data that needs to be sent with the order") +@click.argument('order_items', nargs=-1) +@environment.pass_env +def cli(env, package_keyname, location, preset, name, send_email, complex_type, + extras, order_items): + """Place a quote. + + This CLI command is used for placing a quote of the specified package in + the given location (denoted by a datacenter's long name). Orders made via the CLI + can then be converted to be made programmatically by calling + SoftLayer.OrderingManager.place_quote() with the same keynames. + + Packages for ordering can be retrieved from `slcli order package-list` + Presets for ordering can be retrieved from `slcli order preset-list` (not all packages + have presets) + + Items can be retrieved from `slcli order item-list`. In order to find required + items for the order, use `slcli order category-list`, and then provide the + --category option for each category code in `slcli order item-list`. + + \b + Example: + # Place quote a VSI with 4 CPU, 16 GB RAM, 100 GB SAN disk, + # Ubuntu 16.04, and 1 Gbps public & private uplink in dal13 + slcli order place-quote --name "foobar" --send-email CLOUD_SERVER DALLAS13 \\ + GUEST_CORES_4 \\ + RAM_16_GB \\ + REBOOT_REMOTE_CONSOLE \\ + 1_GBPS_PUBLIC_PRIVATE_NETWORK_UPLINKS \\ + BANDWIDTH_0_GB_2 \\ + 1_IP_ADDRESS \\ + GUEST_DISK_100_GB_SAN \\ + OS_UBUNTU_16_04_LTS_XENIAL_XERUS_MINIMAL_64_BIT_FOR_VSI \\ + MONITORING_HOST_PING \\ + NOTIFICATION_EMAIL_AND_TICKET \\ + AUTOMATED_NOTIFICATION \\ + UNLIMITED_SSL_VPN_USERS_1_PPTP_VPN_USER_PER_ACCOUNT \\ + NESSUS_VULNERABILITY_ASSESSMENT_REPORTING \\ + --extras '{"virtualGuests": [{"hostname": "test", "domain": "softlayer.com"}]}' \\ + --complex-type SoftLayer_Container_Product_Order_Virtual_Guest + + """ + manager = ordering.OrderingManager(env.client) + + if extras: + extras = json.loads(extras) + + args = (package_keyname, location, order_items) + kwargs = {'preset_keyname': preset, + 'extras': extras, + 'quantity': 1, + 'quote_name': name, + 'send_email': send_email, + 'complex_type': complex_type} + + order = manager.place_quote(*args, **kwargs) + + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', order['quote']['id']]) + table.add_row(['name', order['quote']['name']]) + table.add_row(['created', order['orderDate']]) + table.add_row(['expires', order['quote']['expirationDate']]) + table.add_row(['status', order['quote']['status']]) + env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/routes.py python-softlayer-5.6.4/SoftLayer/CLI/routes.py --- python-softlayer-5.4.4/SoftLayer/CLI/routes.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/routes.py 2018-11-16 23:06:56.000000000 +0000 @@ -30,12 +30,16 @@ ('virtual:reload', 'SoftLayer.CLI.virt.reload:cli'), ('virtual:upgrade', 'SoftLayer.CLI.virt.upgrade:cli'), ('virtual:credentials', 'SoftLayer.CLI.virt.credentials:cli'), + ('virtual:capacity', 'SoftLayer.CLI.virt.capacity:cli'), ('dedicatedhost', 'SoftLayer.CLI.dedicatedhost'), ('dedicatedhost:list', 'SoftLayer.CLI.dedicatedhost.list:cli'), ('dedicatedhost:create', 'SoftLayer.CLI.dedicatedhost.create:cli'), ('dedicatedhost:create-options', 'SoftLayer.CLI.dedicatedhost.create_options:cli'), ('dedicatedhost:detail', 'SoftLayer.CLI.dedicatedhost.detail:cli'), + ('dedicatedhost:cancel', 'SoftLayer.CLI.dedicatedhost.cancel:cli'), + ('dedicatedhost:cancel-guests', 'SoftLayer.CLI.dedicatedhost.cancel_guests:cli'), + ('dedicatedhost:list-guests', 'SoftLayer.CLI.dedicatedhost.list_guests:cli'), ('cdn', 'SoftLayer.CLI.cdn'), ('cdn:detail', 'SoftLayer.CLI.cdn.detail:cli'), @@ -210,6 +214,7 @@ ('order:place', 'SoftLayer.CLI.order.place:cli'), ('order:preset-list', 'SoftLayer.CLI.order.preset_list:cli'), ('order:package-locations', 'SoftLayer.CLI.order.package_locations:cli'), + ('order:place-quote', 'SoftLayer.CLI.order.place_quote:cli'), ('rwhois', 'SoftLayer.CLI.rwhois'), ('rwhois:edit', 'SoftLayer.CLI.rwhois.edit:cli'), @@ -282,6 +287,15 @@ ('ticket:attach', 'SoftLayer.CLI.ticket.attach:cli'), ('ticket:detach', 'SoftLayer.CLI.ticket.detach:cli'), + ('user', 'SoftLayer.CLI.user'), + ('user:list', 'SoftLayer.CLI.user.list:cli'), + ('user:detail', 'SoftLayer.CLI.user.detail:cli'), + ('user:permissions', 'SoftLayer.CLI.user.permissions:cli'), + ('user:edit-permissions', 'SoftLayer.CLI.user.edit_permissions:cli'), + ('user:edit-details', 'SoftLayer.CLI.user.edit_details:cli'), + ('user:create', 'SoftLayer.CLI.user.create:cli'), + ('user:delete', 'SoftLayer.CLI.user.delete:cli'), + ('vlan', 'SoftLayer.CLI.vlan'), ('vlan:detail', 'SoftLayer.CLI.vlan.detail:cli'), ('vlan:list', 'SoftLayer.CLI.vlan.list:cli'), diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/securitygroup/list.py python-softlayer-5.6.4/SoftLayer/CLI/securitygroup/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/securitygroup/list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/securitygroup/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -16,8 +16,12 @@ @click.option('--sortby', help='Column to sort by', type=click.Choice(COLUMNS)) +@click.option('--limit', '-l', + help='How many results to get in one api call, default is 100', + default=100, + show_default=True) @environment.pass_env -def cli(env, sortby): +def cli(env, sortby, limit): """List security groups.""" mgr = SoftLayer.NetworkManager(env.client) @@ -25,7 +29,7 @@ table = formatting.Table(COLUMNS) table.sortby = sortby - sgs = mgr.list_securitygroups() + sgs = mgr.list_securitygroups(limit=limit) for secgroup in sgs: table.add_row([ secgroup['id'], diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/securitygroup/rule.py python-softlayer-5.6.4/SoftLayer/CLI/securitygroup/rule.py --- python-softlayer-5.4.4/SoftLayer/CLI/securitygroup/rule.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/securitygroup/rule.py 2018-11-16 23:06:56.000000000 +0000 @@ -15,7 +15,9 @@ 'ethertype', 'portRangeMin', 'portRangeMax', - 'protocol'] + 'protocol', + 'createDate', + 'modifyDate'] @click.command() @@ -49,7 +51,9 @@ rule.get('ethertype') or formatting.blank(), port_min, port_max, - rule.get('protocol') or formatting.blank() + rule.get('protocol') or formatting.blank(), + rule.get('createDate') or formatting.blank(), + rule.get('modifyDate') or formatting.blank() ]) env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/subnet/list.py python-softlayer-5.6.4/SoftLayer/CLI/subnet/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/subnet/list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/subnet/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -26,11 +26,10 @@ @click.option('--identifier', help="Filter by network identifier") @click.option('--subnet-type', '-t', help="Filter by subnet type") @click.option('--network-space', help="Filter by network space") -@click.option('--v4', '--ipv4', is_flag=True, help="Display only IPv4 subnets") -@click.option('--v6', '--ipv6', is_flag=True, help="Display only IPv6 subnets") +@click.option('--ipv4', '--v4', is_flag=True, help="Display only IPv4 subnets") +@click.option('--ipv6', '--v6', is_flag=True, help="Display only IPv6 subnets") @environment.pass_env -def cli(env, sortby, datacenter, identifier, subnet_type, network_space, - ipv4, ipv6): +def cli(env, sortby, datacenter, identifier, subnet_type, network_space, ipv4, ipv6): """List subnets.""" mgr = SoftLayer.NetworkManager(env.client) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/ticket/create.py python-softlayer-5.6.4/SoftLayer/CLI/ticket/create.py --- python-softlayer-5.4.4/SoftLayer/CLI/ticket/create.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/ticket/create.py 2018-11-16 23:06:56.000000000 +0000 @@ -11,43 +11,38 @@ @click.command() @click.option('--title', required=True, help="The title of the ticket") -@click.option('--subject-id', - type=int, - required=True, +@click.option('--subject-id', type=int, required=True, help="""The subject id to use for the ticket, issue 'slcli ticket subjects' to get the list""") @click.option('--body', help="The ticket body") -@click.option('--hardware', - 'hardware_identifier', +@click.option('--hardware', 'hardware_identifier', help="The identifier for hardware to attach") -@click.option('--virtual', - 'virtual_identifier', +@click.option('--virtual', 'virtual_identifier', help="The identifier for a virtual server to attach") +@click.option('--priority', 'priority', type=click.Choice(['1', '2', '3', '4']), default=None, + help="""Ticket priority, from 1 (Critical) to 4 (Minimal Impact). + Only settable with Advanced and Premium support. See https://www.ibm.com/cloud/support""") @environment.pass_env -def cli(env, title, subject_id, body, hardware_identifier, virtual_identifier): +def cli(env, title, subject_id, body, hardware_identifier, virtual_identifier, priority): """Create a support ticket.""" ticket_mgr = SoftLayer.TicketManager(env.client) if body is None: body = click.edit('\n\n' + ticket.TEMPLATE_MSG) - created_ticket = ticket_mgr.create_ticket( title=title, body=body, - subject=subject_id) + subject=subject_id, + priority=priority) if hardware_identifier: hardware_mgr = SoftLayer.HardwareManager(env.client) - hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids, - hardware_identifier, - 'hardware') + hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids, hardware_identifier, 'hardware') ticket_mgr.attach_hardware(created_ticket['id'], hardware_id) if virtual_identifier: vs_mgr = SoftLayer.VSManager(env.client) - vs_id = helpers.resolve_id(vs_mgr.resolve_ids, - virtual_identifier, - 'VS') + vs_id = helpers.resolve_id(vs_mgr.resolve_ids, virtual_identifier, 'VS') ticket_mgr.attach_virtual_server(created_ticket['id'], vs_id) env.fout(ticket.get_ticket_results(ticket_mgr, created_ticket['id'])) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/ticket/__init__.py python-softlayer-5.6.4/SoftLayer/CLI/ticket/__init__.py --- python-softlayer-5.4.4/SoftLayer/CLI/ticket/__init__.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/ticket/__init__.py 2018-11-16 23:06:56.000000000 +0000 @@ -7,6 +7,15 @@ TEMPLATE_MSG = "***** SoftLayer Ticket Content ******" +# https://softlayer.github.io/reference/services/SoftLayer_Ticket_Priority/getPriorities/ +PRIORITY_MAP = [ + 'No Priority', + 'Severity 1 - Critical Impact / Service Down', + 'Severity 2 - Significant Business Impact', + 'Severity 3 - Minor Business Impact', + 'Severity 4 - Minimal Business Impact' +] + def get_ticket_results(mgr, ticket_id, update_count=1): """Get output about a ticket. @@ -24,6 +33,7 @@ table.add_row(['id', ticket['id']]) table.add_row(['title', ticket['title']]) + table.add_row(['priority', PRIORITY_MAP[ticket.get('priority', 0)]]) if ticket.get('assignedUser'): user = ticket['assignedUser'] table.add_row([ diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/ticket/list.py python-softlayer-5.6.4/SoftLayer/CLI/ticket/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/ticket/list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/ticket/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -9,25 +9,21 @@ @click.command() -@click.option('--open / --closed', 'is_open', - help="Display only open or closed tickets", - default=True) +@click.option('--open / --closed', 'is_open', default=True, + help="Display only open or closed tickets") @environment.pass_env def cli(env, is_open): """List tickets.""" ticket_mgr = SoftLayer.TicketManager(env.client) + table = formatting.Table([ + 'id', 'assigned_user', 'title', 'last_edited', 'status', 'updates', 'priority' + ]) - tickets = ticket_mgr.list_tickets(open_status=is_open, - closed_status=not is_open) - - table = formatting.Table(['id', 'assigned_user', 'title', - 'last_edited', 'status']) - + tickets = ticket_mgr.list_tickets(open_status=is_open, closed_status=not is_open) for ticket in tickets: user = formatting.blank() if ticket.get('assignedUser'): - user = "%s %s" % (ticket['assignedUser']['firstName'], - ticket['assignedUser']['lastName']) + user = "%s %s" % (ticket['assignedUser']['firstName'], ticket['assignedUser']['lastName']) table.add_row([ ticket['id'], @@ -35,6 +31,8 @@ click.wrap_text(ticket['title']), ticket['lastEditDate'], ticket['status']['name'], + ticket.get('updateCount', 0), + ticket.get('priority', 0) ]) env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/create.py python-softlayer-5.6.4/SoftLayer/CLI/user/create.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/create.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/create.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,100 @@ +"""Creates a user """ +# :license: MIT, see LICENSE for more details. + +import json +import string +import sys + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('username') +@click.option('--email', '-e', required=True, + help="Email address for this user. Required for creation.") +@click.option('--password', '-p', default=None, show_default=True, + help="Password to set for this user. If no password is provided, user will be sent an email " + "to generate one, which expires in 24 hours. '-p generate' will create a password for you " + "(Requires Python 3.6+). Passwords require 8+ characters, upper and lowercase, a number " + "and a symbol.") +@click.option('--from-user', '-u', default=None, + help="Base user to use as a template for creating this user. " + "Will default to the user running this command. Information provided in --template " + "supersedes this template.") +@click.option('--template', '-t', default=None, + help="A json string describing https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/") +@click.option('--api-key', '-a', default=False, is_flag=True, help="Create an API key for this user.") +@environment.pass_env +def cli(env, username, email, password, from_user, template, api_key): + """Creates a user Users. + + :Example: slcli user create my@email.com -e my@email.com -p generate -a + -t '{"firstName": "Test", "lastName": "Testerson"}' + + Remember to set the permissions and access for this new user. + """ + + mgr = SoftLayer.UserManager(env.client) + user_mask = ("mask[id, firstName, lastName, email, companyName, address1, city, country, postalCode, " + "state, userStatusId, timezoneId]") + from_user_id = None + if from_user is None: + user_template = mgr.get_current_user(objectmask=user_mask) + from_user_id = user_template['id'] + else: + from_user_id = helpers.resolve_id(mgr.resolve_ids, from_user, 'username') + user_template = mgr.get_user(from_user_id, objectmask=user_mask) + # If we send the ID back to the API, an exception will be thrown + del user_template['id'] + + if template is not None: + try: + template_object = json.loads(template) + for key in template_object: + user_template[key] = template_object[key] + except ValueError as ex: + raise exceptions.ArgumentError("Unable to parse --template. %s" % ex) + + user_template['username'] = username + if password == 'generate': + password = generate_password() + + user_template['email'] = email + + if not env.skip_confirmations: + table = formatting.KeyValueTable(['name', 'value']) + for key in user_template: + table.add_row([key, user_template[key]]) + table.add_row(['password', password]) + click.secho("You are about to create the following user...", fg='green') + env.fout(table) + if not formatting.confirm("Do you wish to continue?"): + raise exceptions.CLIAbort("Canceling creation!") + + result = mgr.create_user(user_template, password) + new_api_key = None + if api_key: + click.secho("Adding API key...", fg='green') + new_api_key = mgr.add_api_authentication_key(result['id']) + + table = formatting.Table(['Username', 'Email', 'Password', 'API Key']) + table.add_row([result['username'], result['email'], password, new_api_key]) + env.fout(table) + + +def generate_password(): + """Returns a 23 character random string, with 3 special characters at the end""" + if sys.version_info > (3, 6): + import secrets # pylint: disable=import-error + alphabet = string.ascii_letters + string.digits + password = ''.join(secrets.choice(alphabet) for i in range(20)) + special = ''.join(secrets.choice(string.punctuation) for i in range(3)) + return password + special + else: + raise ImportError("Generating passwords require python 3.6 or higher") diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/delete.py python-softlayer-5.6.4/SoftLayer/CLI/user/delete.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/delete.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/delete.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,32 @@ +"""Delete user.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Sets a user's status to CANCEL_PENDING, which will immediately disable the account, + + and will eventually be fully removed from the account by an automated internal process. + + Example: slcli user delete userId + """ + + mgr = SoftLayer.UserManager(env.client) + + user_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'username') + + user_template = {'userStatusId': 1021} + + result = mgr.edit_user(user_id, user_template) + if result: + click.secho("%s deleted successfully" % identifier, fg='green') + else: + click.secho("Failed to delete %s" % identifier, fg='red') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/detail.py python-softlayer-5.6.4/SoftLayer/CLI/user/detail.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/detail.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/detail.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,153 @@ +"""User details.""" +# :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() +@click.argument('identifier') +@click.option('--keys', '-k', is_flag=True, default=False, + help="Show the users API key.") +@click.option('--permissions', '-p', is_flag=True, default=False, + help="Display permissions assigned to this user. Master users will show no permissions") +@click.option('--hardware', '-h', is_flag=True, default=False, + help="Display hardware this user has access to.") +@click.option('--virtual', '-v', is_flag=True, default=False, + help="Display virtual guests this user has access to.") +@click.option('--logins', '-l', is_flag=True, default=False, + help="Show login history of this user for the last 30 days") +@click.option('--events', '-e', is_flag=True, default=False, + help="Show audit log for this user.") +@environment.pass_env +def cli(env, identifier, keys, permissions, hardware, virtual, logins, events): + """User details.""" + + mgr = SoftLayer.UserManager(env.client) + user_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'username') + object_mask = "userStatus[name], parent[id, username], apiAuthenticationKeys[authenticationKey], "\ + "unsuccessfulLogins, successfulLogins" + + user = mgr.get_user(user_id, object_mask) + env.fout(basic_info(user, keys)) + + if permissions: + perms = mgr.get_user_permissions(user_id) + env.fout(print_permissions(perms)) + if hardware: + mask = "id, hardware, dedicatedHosts" + access = mgr.get_user(user_id, mask) + env.fout(print_dedicated_access(access.get('dedicatedHosts', []))) + env.fout(print_access(access.get('hardware', []), 'Hardware')) + if virtual: + mask = "id, virtualGuests" + access = mgr.get_user(user_id, mask) + env.fout(print_access(access.get('virtualGuests', []), 'Virtual Guests')) + if logins: + login_log = mgr.get_logins(user_id) + env.fout(print_logins(login_log)) + if events: + event_log = mgr.get_events(user_id) + env.fout(print_events(event_log)) + + +def basic_info(user, keys): + """Prints a table of basic user information""" + + table = formatting.KeyValueTable(['Title', 'Basic Information']) + table.align['Title'] = 'r' + table.align['Basic Information'] = 'l' + + table.add_row(['Id', user.get('id', '-')]) + table.add_row(['Username', user.get('username', '-')]) + if keys: + for key in user.get('apiAuthenticationKeys'): + table.add_row(['APIKEY', key.get('authenticationKey')]) + table.add_row(['Name', "%s %s" % (user.get('firstName', '-'), user.get('lastName', '-'))]) + table.add_row(['Email', user.get('email')]) + table.add_row(['OpenID', user.get('openIdConnectUserName')]) + address = "%s %s %s %s %s %s" % ( + user.get('address1'), user.get('address2'), user.get('city'), user.get('state'), + user.get('country'), user.get('postalCode')) + table.add_row(['Address', address]) + table.add_row(['Company', user.get('companyName')]) + table.add_row(['Created', user.get('createDate')]) + table.add_row(['Phone Number', user.get('officePhone')]) + if user.get('parentId', False): + table.add_row(['Parent User', utils.lookup(user, 'parent', 'username')]) + table.add_row(['Status', utils.lookup(user, 'userStatus', 'name')]) + table.add_row(['PPTP VPN', user.get('pptpVpnAllowedFlag', 'No')]) + table.add_row(['SSL VPN', user.get('sslVpnAllowedFlag', 'No')]) + for login in user.get('unsuccessfulLogins', {}): + login_string = "%s From: %s" % (login.get('createDate'), login.get('ipAddress')) + table.add_row(['Last Failed Login', login_string]) + break + for login in user.get('successfulLogins', {}): + login_string = "%s From: %s" % (login.get('createDate'), login.get('ipAddress')) + table.add_row(['Last Login', login_string]) + break + + return table + + +def print_permissions(permissions): + """Prints out a users permissions""" + + table = formatting.Table(['keyName', 'Description']) + for perm in permissions: + table.add_row([perm['keyName'], perm['name']]) + return table + + +def print_access(access, title): + """Prints out the hardware or virtual guests a user can access""" + + columns = ['id', 'hostname', 'Primary Public IP', 'Primary Private IP', 'Created'] + table = formatting.Table(columns, title) + + for host in access: + host_id = host.get('id') + host_fqdn = host.get('fullyQualifiedDomainName', '-') + host_primary = host.get('primaryIpAddress') + host_private = host.get('primaryBackendIpAddress') + host_created = host.get('provisionDate') + table.add_row([host_id, host_fqdn, host_primary, host_private, host_created]) + return table + + +def print_dedicated_access(access): + """Prints out the dedicated hosts a user can access""" + + table = formatting.Table(['id', 'Name', 'Cpus', 'Memory', 'Disk', 'Created'], 'Dedicated Access') + for host in access: + host_id = host.get('id') + host_fqdn = host.get('name') + host_cpu = host.get('cpuCount') + host_mem = host.get('memoryCapacity') + host_disk = host.get('diskCapacity') + host_created = host.get('createDate') + table.add_row([host_id, host_fqdn, host_cpu, host_mem, host_disk, host_created]) + return table + + +def print_logins(logins): + """Prints out the login history for a user""" + table = formatting.Table(['Date', 'IP Address', 'Successufl Login?']) + for login in logins: + table.add_row([login.get('createDate'), login.get('ipAddress'), login.get('successFlag')]) + return table + + +def print_events(events): + """Prints out the event log for a user""" + columns = ['Date', 'Type', 'IP Address', 'label', 'username'] + table = formatting.Table(columns) + for event in events: + table.add_row([event.get('eventCreateDate'), event.get('eventName'), + event.get('ipAddress'), event.get('label'), event.get('username')]) + return table diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/edit_details.py python-softlayer-5.6.4/SoftLayer/CLI/user/edit_details.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/edit_details.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/edit_details.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,43 @@ +"""List Users.""" +# :license: MIT, see LICENSE for more details. + + +import json + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('user') +@click.option('--template', '-t', required=True, + help="A json string describing https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/") +@environment.pass_env +def cli(env, user, template): + """Edit a Users details + + JSON strings should be enclosed in '' and each item should be enclosed in "" + + :Example: slcli user edit-details testUser -t '{"firstName": "Test", "lastName": "Testerson"}' + """ + mgr = SoftLayer.UserManager(env.client) + user_id = helpers.resolve_id(mgr.resolve_ids, user, 'username') + + user_template = {} + if template is not None: + try: + template_object = json.loads(template) + for key in template_object: + user_template[key] = template_object[key] + except ValueError as ex: + raise exceptions.ArgumentError("Unable to parse --template. %s" % ex) + + result = mgr.edit_user(user_id, user_template) + if result: + click.secho("%s updated successfully" % (user), fg='green') + else: + click.secho("Failed to update %s" % (user), fg='red') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/edit_permissions.py python-softlayer-5.6.4/SoftLayer/CLI/user/edit_permissions.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/edit_permissions.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/edit_permissions.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,38 @@ +"""Enable or Disable specific permissions for a user""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@click.option('--enable/--disable', default=True, + help="Enable (DEFAULT) or Disable selected permissions") +@click.option('--permission', '-p', multiple=True, + help="Permission keyName to set, multiple instances allowed. " + "Use keyword ALL to select ALL permisssions") +@click.option('--from-user', '-u', default=None, + help="Set permissions to match this user's permissions. " + "Will add then remove the appropriate permissions") +@environment.pass_env +def cli(env, identifier, enable, permission, from_user): + """Enable or Disable specific permissions.""" + mgr = SoftLayer.UserManager(env.client) + user_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'username') + result = False + if from_user: + from_user_id = helpers.resolve_id(mgr.resolve_ids, from_user, 'username') + result = mgr.permissions_from_user(user_id, from_user_id) + elif enable: + result = mgr.add_permissions(user_id, permission) + else: + result = mgr.remove_permissions(user_id, permission) + + if result: + click.secho("Permissions updated successfully: %s" % ", ".join(permission), fg='green') + else: + click.secho("Failed to update permissions: %s" % ", ".join(permission), fg='red') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/__init__.py python-softlayer-5.6.4/SoftLayer/CLI/user/__init__.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/__init__.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1 @@ +"""Manage Users.""" diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/list.py python-softlayer-5.6.4/SoftLayer/CLI/user/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/list.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,48 @@ +"""List Users.""" +# :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',)), + column_helper.Column('username', ('username',)), + column_helper.Column('email', ('email',)), + column_helper.Column('displayName', ('displayName',)), + column_helper.Column('status', ('userStatus', 'name')), + column_helper.Column('hardwareCount', ('hardwareCount',)), + column_helper.Column('virtualGuestCount', ('virtualGuestCount',)) +] + +DEFAULT_COLUMNS = [ + 'id', + 'username', + 'email', + 'displayName' +] + + +@click.command() +@click.option('--columns', + callback=column_helper.get_formatter(COLUMNS), + help='Columns to display. [options: %s]' % ', '.join(column.name for column in COLUMNS), + default=','.join(DEFAULT_COLUMNS), + show_default=True) +@environment.pass_env +def cli(env, columns): + """List Users.""" + + mgr = SoftLayer.UserManager(env.client) + users = mgr.list_users() + + table = formatting.Table(columns.columns) + for user in users: + table.add_row([value or formatting.blank() + for value in columns.row(user)]) + + env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/user/permissions.py python-softlayer-5.6.4/SoftLayer/CLI/user/permissions.py --- python-softlayer-5.4.4/SoftLayer/CLI/user/permissions.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/user/permissions.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,57 @@ +"""List A users permissions.""" +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """User Permissions. TODO change to list all permissions, and which users have them""" + + mgr = SoftLayer.UserManager(env.client) + user_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'username') + object_mask = "mask[id, permissions, isMasterUserFlag, roles]" + + user = mgr.get_user(user_id, object_mask) + all_permissions = mgr.get_all_permissions() + user_permissions = perms_to_dict(user['permissions']) + + if user['isMasterUserFlag']: + click.secho('This account is the Master User and has all permissions enabled', fg='green') + + env.fout(roles_table(user)) + env.fout(permission_table(user_permissions, all_permissions)) + + +def perms_to_dict(perms): + """Takes a list of permissions and transforms it into a dictionary for better searching""" + permission_dict = {} + for perm in perms: + permission_dict[perm['keyName']] = True + return permission_dict + + +def permission_table(user_permissions, all_permissions): + """Creates a table of available permissions""" + + table = formatting.Table(['Description', 'KeyName', 'Assigned']) + table.align['KeyName'] = 'l' + table.align['Description'] = 'l' + table.align['Assigned'] = 'l' + for perm in all_permissions: + assigned = user_permissions.get(perm['keyName'], False) + table.add_row([perm['name'], perm['keyName'], assigned]) + return table + + +def roles_table(user): + """Creates a table for a users roles""" + table = formatting.Table(['id', 'Role Name', 'Description']) + for role in user['roles']: + table.add_row([role['id'], role['name'], role['description']]) + return table diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/create_guest.py python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/create_guest.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/create_guest.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/create_guest.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,63 @@ +"""List Reserved Capacity""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer.CLI.virt.create import _parse_create_args as _parse_create_args +from SoftLayer.CLI.virt.create import _update_with_like_args as _update_with_like_args +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command() +@click.option('--capacity-id', type=click.INT, help="Reserve capacity Id to provision this guest into.") +@click.option('--primary-disk', type=click.Choice(['25', '100']), default='25', help="Size of the main drive.") +@click.option('--hostname', '-H', required=True, prompt=True, help="Host portion of the FQDN.") +@click.option('--domain', '-D', required=True, prompt=True, help="Domain portion of the FQDN.") +@click.option('--os', '-o', help="OS install code. Tip: you can specify _LATEST.") +@click.option('--image', help="Image ID. See: 'slcli image list' for reference.") +@click.option('--boot-mode', type=click.STRING, + help="Specify the mode to boot the OS in. Supported modes are HVM and PV.") +@click.option('--postinstall', '-i', help="Post-install script to download.") +@helpers.multi_option('--key', '-k', help="SSH keys to add to the root user.") +@helpers.multi_option('--disk', help="Additional disk sizes.") +@click.option('--private', is_flag=True, help="Forces the VS to only have access the private network.") +@click.option('--like', is_eager=True, callback=_update_with_like_args, + help="Use the configuration from an existing VS.") +@click.option('--network', '-n', help="Network port speed in Mbps.") +@helpers.multi_option('--tag', '-g', help="Tags to add to the instance.") +@click.option('--userdata', '-u', help="User defined metadata string.") +@click.option('--ipv6', is_flag=True, help="Adds an IPv6 address to this guest") +@click.option('--test', is_flag=True, + help="Test order, will return the order container, but not actually order a server.") +@environment.pass_env +def cli(env, **args): + """Allows for creating a virtual guest in a reserved capacity.""" + create_args = _parse_create_args(env.client, args) + if args.get('ipv6'): + create_args['ipv6'] = True + create_args['primary_disk'] = args.get('primary_disk') + manager = CapacityManager(env.client) + capacity_id = args.get('capacity_id') + test = args.get('test') + + result = manager.create_guest(capacity_id, test, create_args) + + env.fout(_build_receipt(result, test)) + + +def _build_receipt(result, test=False): + title = "OrderId: %s" % (result.get('orderId', 'No order placed')) + table = formatting.Table(['Item Id', 'Description'], title=title) + table.align['Description'] = 'l' + + if test: + prices = result['prices'] + else: + prices = result['orderDetails']['prices'] + + for item in prices: + table.add_row([item['id'], item['item']['description']]) + return table diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/create_options.py python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/create_options.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/create_options.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/create_options.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,45 @@ +"""List options for creating Reserved Capacity""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command() +@environment.pass_env +def cli(env): + """List options for creating Reserved Capacity""" + manager = CapacityManager(env.client) + items = manager.get_create_options() + + items.sort(key=lambda term: int(term['capacity'])) + table = formatting.Table(["KeyName", "Description", "Term", "Default Hourly Price Per Instance"], + title="Reserved Capacity Options") + table.align["Hourly Price"] = "l" + table.align["Description"] = "l" + table.align["KeyName"] = "l" + for item in items: + table.add_row([ + item['keyName'], item['description'], item['capacity'], get_price(item) + ]) + env.fout(table) + + regions = manager.get_available_routers() + location_table = formatting.Table(['Location', 'POD', 'BackendRouterId'], 'Orderable Locations') + for region in regions: + for location in region['locations']: + for pod in location['location']['pods']: + location_table.add_row([region['keyname'], pod['backendRouterName'], pod['backendRouterId']]) + env.fout(location_table) + + +def get_price(item): + """Finds the price with the default locationGroupId""" + the_price = "No Default Pricing" + for price in item.get('prices', []): + if not price.get('locationGroupId'): + the_price = "%0.4f" % float(price['hourlyRecurringFee']) + return the_price diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/create.py python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/create.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/create.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/create.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,51 @@ +"""Create a Reserved Capacity instance.""" + +import click + + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command(epilog=click.style("""WARNING: Reserved Capacity is on a yearly contract""" + """ and not cancelable until the contract is expired.""", fg='red')) +@click.option('--name', '-n', required=True, prompt=True, + help="Name for your new reserved capacity") +@click.option('--backend_router_id', '-b', required=True, prompt=True, type=int, + help="backendRouterId, create-options has a list of valid ids to use.") +@click.option('--flavor', '-f', required=True, prompt=True, + help="Capacity keyname (C1_2X2_1_YEAR_TERM for example).") +@click.option('--instances', '-i', required=True, prompt=True, type=int, + help="Number of VSI instances this capacity reservation can support.") +@click.option('--test', is_flag=True, + help="Do not actually create the virtual server") +@environment.pass_env +def cli(env, name, backend_router_id, flavor, instances, test=False): + """Create a Reserved Capacity instance. + + *WARNING*: Reserved Capacity is on a yearly contract and not cancelable until the contract is expired. + """ + manager = CapacityManager(env.client) + + result = manager.create( + name=name, + backend_router_id=backend_router_id, + flavor=flavor, + instances=instances, + test=test) + if test: + table = formatting.Table(['Name', 'Value'], "Test Order") + container = result['orderContainers'][0] + table.add_row(['Name', container['name']]) + table.add_row(['Location', container['locationObject']['longName']]) + for price in container['prices']: + table.add_row(['Contract', price['item']['description']]) + table.add_row(['Hourly Total', result['postTaxRecurring']]) + else: + table = formatting.Table(['Name', 'Value'], "Reciept") + table.add_row(['Order Date', result['orderDate']]) + table.add_row(['Order ID', result['orderId']]) + table.add_row(['status', result['placedOrder']['status']]) + table.add_row(['Hourly Total', result['orderDetails']['postTaxRecurring']]) + env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/detail.py python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/detail.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/detail.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/detail.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,57 @@ +"""Shows the details of a reserved capacity group""" + +import click + +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + +COLUMNS = [ + column_helper.Column('Id', ('id',)), + column_helper.Column('hostname', ('hostname',)), + column_helper.Column('domain', ('domain',)), + column_helper.Column('primary_ip', ('primaryIpAddress',)), + column_helper.Column('backend_ip', ('primaryBackendIpAddress',)), +] + +DEFAULT_COLUMNS = [ + 'id', + 'hostname', + 'domain', + 'primary_ip', + 'backend_ip' +] + + +@click.command(epilog="Once provisioned, virtual guests can be managed with the slcli vs commands") +@click.argument('identifier') +@click.option('--columns', + callback=column_helper.get_formatter(COLUMNS), + help='Columns to display. [options: %s]' + % ', '.join(column.name for column in COLUMNS), + default=','.join(DEFAULT_COLUMNS), + show_default=True) +@environment.pass_env +def cli(env, identifier, columns): + """Reserved Capacity Group details. Will show which guests are assigned to a reservation.""" + + manager = CapacityManager(env.client) + mask = """mask[instances[id,createDate,guestId,billingItem[id, description, recurringFee, category[name]], + guest[modifyDate,id, primaryBackendIpAddress, primaryIpAddress,domain, hostname]]]""" + result = manager.get_object(identifier, mask) + + try: + flavor = result['instances'][0]['billingItem']['description'] + except KeyError: + flavor = "Pending Approval..." + + table = formatting.Table(columns.columns, title="%s - %s" % (result.get('name'), flavor)) + # RCI = Reserved Capacity Instance + for rci in result['instances']: + guest = rci.get('guest', None) + if guest is not None: + table.add_row([value or formatting.blank() for value in columns.row(guest)]) + else: + table.add_row(['-' for value in columns.columns]) + env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/__init__.py python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/__init__.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/__init__.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,48 @@ +"""Manages Reserved Capacity.""" +# :license: MIT, see LICENSE for more details. + +import importlib +import os + +import click + +CONTEXT = {'help_option_names': ['-h', '--help'], + 'max_content_width': 999} + + +class CapacityCommands(click.MultiCommand): + """Loads module for capacity related commands. + + Will automatically replace _ with - where appropriate. + I'm not sure if this is better or worse than using a long list of manual routes, so I'm trying it here. + CLI/virt/capacity/create_guest.py -> slcli vs capacity create-guest + """ + + def __init__(self, **attrs): + click.MultiCommand.__init__(self, **attrs) + self.path = os.path.dirname(__file__) + + def list_commands(self, ctx): + """List all sub-commands.""" + commands = [] + for filename in os.listdir(self.path): + if filename == '__init__.py': + continue + if filename.endswith('.py'): + commands.append(filename[:-3].replace("_", "-")) + commands.sort() + return commands + + def get_command(self, ctx, cmd_name): + """Get command for click.""" + path = "%s.%s" % (__name__, cmd_name) + path = path.replace("-", "_") + module = importlib.import_module(path) + return getattr(module, 'cli') + + +# Required to get the sub-sub-sub command to work. +@click.group(cls=CapacityCommands, context_settings=CONTEXT) +def cli(): + """Base command for all capacity related concerns""" + pass diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/list.py python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/capacity/list.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/capacity/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,32 @@ +"""List Reserved Capacity""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command() +@environment.pass_env +def cli(env): + """List Reserved Capacity groups.""" + manager = CapacityManager(env.client) + result = manager.list() + table = formatting.Table( + ["ID", "Name", "Capacity", "Flavor", "Location", "Created"], + title="Reserved Capacity" + ) + for r_c in result: + occupied_string = "#" * int(r_c.get('occupiedInstanceCount', 0)) + available_string = "-" * int(r_c.get('availableInstanceCount', 0)) + + try: + flavor = r_c['instances'][0]['billingItem']['description'] + # cost = float(r_c['instances'][0]['billingItem']['hourlyRecurringFee']) + except KeyError: + flavor = "Unknown Billing Item" + location = r_c['backendRouter']['hostname'] + capacity = "%s%s" % (occupied_string, available_string) + table.add_row([r_c['id'], r_c['name'], capacity, flavor, location, r_c['createDate']]) + env.fout(table) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/create_options.py python-softlayer-5.6.4/SoftLayer/CLI/virt/create_options.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/create_options.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/create_options.py 2018-11-16 23:06:56.000000000 +0000 @@ -1,5 +1,6 @@ """Virtual server order options.""" # :license: MIT, see LICENSE for more details. +# pylint: disable=too-many-statements import os import os.path diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/create.py python-softlayer-5.6.4/SoftLayer/CLI/virt/create.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/create.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/create.py 2018-11-16 23:06:56.000000000 +0000 @@ -72,11 +72,11 @@ :param dict args: CLI arguments """ data = { - "hourly": args['billing'] == 'hourly', + "hourly": args.get('billing', 'hourly') == 'hourly', "domain": args['domain'], "hostname": args['hostname'], - "private": args['private'], - "dedicated": args['dedicated'], + "private": args.get('private', None), + "dedicated": args.get('dedicated', None), "disks": args['disk'], "cpus": args.get('cpu', None), "memory": args.get('memory', None), @@ -89,7 +89,7 @@ if not args.get('san') and args.get('flavor'): data['local_disk'] = None else: - data['local_disk'] = not args['san'] + data['local_disk'] = not args.get('san') if args.get('os'): data['os_code'] = args['os'] @@ -133,6 +133,10 @@ if args.get('vlan_private'): data['private_vlan'] = args['vlan_private'] + data['public_subnet'] = args.get('subnet_public', None) + + data['private_subnet'] = args.get('subnet_private', None) + if args.get('public_security_group'): pub_groups = args.get('public_security_group') data['public_security_groups'] = [group for group in pub_groups] @@ -225,12 +229,18 @@ type=click.Path(exists=True, readable=True, resolve_path=True)) @click.option('--vlan-public', help="The ID of the public VLAN on which you want the virtual " - "server placed", + "server placed", type=click.INT) @click.option('--vlan-private', help="The ID of the private VLAN on which you want the virtual " "server placed", type=click.INT) +@click.option('--subnet-public', + help="The ID of the public SUBNET on which you want the virtual server placed", + type=click.INT) +@click.option('--subnet-private', + help="The ID of the private SUBNET on which you want the virtual server placed", + type=click.INT) @helpers.multi_option('--public-security-group', '-S', help=('Security group ID to associate with ' @@ -260,33 +270,19 @@ output = [] if args.get('test'): result = vsi.verify_create_instance(**data) - total_monthly = 0.0 - total_hourly = 0.0 - table = formatting.Table(['Item', 'cost']) - table.align['Item'] = 'r' - table.align['cost'] = 'r' - - for price in result['prices']: - total_monthly += float(price.get('recurringFee', 0.0)) - total_hourly += float(price.get('hourlyRecurringFee', 0.0)) - if args.get('billing') == 'hourly': - rate = "%.2f" % float(price['hourlyRecurringFee']) - elif args.get('billing') == 'monthly': - rate = "%.2f" % float(price['recurringFee']) - - table.add_row([price['item']['description'], rate]) - - total = 0 - if args.get('billing') == 'hourly': - total = total_hourly - elif args.get('billing') == 'monthly': - total = total_monthly - - billing_rate = 'monthly' - if args.get('billing') == 'hourly': - billing_rate = 'hourly' - table.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) + if result['presetId']: + ordering_mgr = SoftLayer.OrderingManager(env.client) + item_prices = ordering_mgr.get_item_prices(result['packageId']) + preset_prices = ordering_mgr.get_preset_prices(result['presetId']) + search_keys = ["guest_core", "ram"] + for price in preset_prices['prices']: + if price['item']['itemCategory']['categoryCode'] in search_keys: + item_key_name = price['item']['keyName'] + _add_item_prices(item_key_name, item_prices, result) + + table = _build_receipt_table(result['prices'], args.get('billing')) + output.append(table) output.append(formatting.FormattedItem( None, @@ -324,6 +320,35 @@ env.fout(output) +def _add_item_prices(item_key_name, item_prices, result): + """Add the flavor item prices to the rest o the items prices""" + for item in item_prices: + if item_key_name == item['item']['keyName']: + if 'pricingLocationGroup' in item: + for location in item['pricingLocationGroup']['locations']: + if result['location'] == str(location['id']): + result['prices'].append(item) + + +def _build_receipt_table(prices, billing="hourly"): + """Retrieve the total recurring fee of the items prices""" + total = 0.000 + table = formatting.Table(['Cost', 'Item']) + table.align['Cost'] = 'r' + table.align['Item'] = 'l' + for price in prices: + rate = 0.000 + if billing == "hourly": + rate += float(price.get('hourlyRecurringFee', 0.000)) + else: + rate += float(price.get('recurringFee', 0.000)) + total += rate + + table.add_row(["%.3f" % rate, price['item']['description']]) + table.add_row(["%.3f" % total, "Total %s cost" % billing]) + return table + + def _validate_args(env, args): """Raises an ArgumentError if the given arguments are not valid.""" diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/__init__.py python-softlayer-5.6.4/SoftLayer/CLI/virt/__init__.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/__init__.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/__init__.py 2018-11-16 23:06:56.000000000 +0000 @@ -30,4 +30,5 @@ elif unit in ['g', 'gb']: return amount * 1024 + MEM_TYPE = MemoryType() diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/list.py python-softlayer-5.6.4/SoftLayer/CLI/virt/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -62,9 +62,13 @@ % ', '.join(column.name for column in COLUMNS), default=','.join(DEFAULT_COLUMNS), show_default=True) +@click.option('--limit', '-l', + help='How many results to get in one api call, default is 100', + default=100, + show_default=True) @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tag, columns): + hourly, monthly, tag, columns, limit): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) @@ -77,11 +81,11 @@ datacenter=datacenter, nic_speed=network, tags=tag, - mask=columns.mask()) + mask=columns.mask(), + limit=limit) table = formatting.Table(columns.columns) table.sortby = sortby - for guest in guests: table.add_row([value or formatting.blank() for value in columns.row(guest)]) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/virt/upgrade.py python-softlayer-5.6.4/SoftLayer/CLI/virt/upgrade.py --- python-softlayer-5.4.4/SoftLayer/CLI/virt/upgrade.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/virt/upgrade.py 2018-11-16 23:06:56.000000000 +0000 @@ -21,15 +21,17 @@ help="CPU core will be on a dedicated host server.") @click.option('--memory', type=virt.MEM_TYPE, help="Memory in megabytes") @click.option('--network', type=click.INT, help="Network port speed in Mbps") +@click.option('--flavor', type=click.STRING, help="Flavor keyName\n" + "Do not use --memory, --cpu or --private, if you are using flavors") @environment.pass_env -def cli(env, identifier, cpu, private, memory, network): +def cli(env, identifier, cpu, private, memory, network, flavor): """Upgrade a virtual server.""" vsi = SoftLayer.VSManager(env.client) - if not any([cpu, memory, network]): + if not any([cpu, memory, network, flavor]): raise exceptions.ArgumentError( - "Must provide [--cpu], [--memory], or [--network] to upgrade") + "Must provide [--cpu], [--memory], [--network], or [--flavor] to upgrade") if private and not cpu: raise exceptions.ArgumentError( @@ -48,5 +50,6 @@ cpus=cpu, memory=memory, nic_speed=network, - public=not private): + public=not private, + preset=flavor): raise exceptions.CLIAbort('VS Upgrade Failed') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/vlan/detail.py python-softlayer-5.6.4/SoftLayer/CLI/vlan/detail.py --- python-softlayer-5.4.4/SoftLayer/CLI/vlan/detail.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/vlan/detail.py 2018-11-16 23:06:56.000000000 +0000 @@ -60,8 +60,8 @@ if vlan.get('virtualGuests'): vs_table = formatting.KeyValueTable(server_columns) for vsi in vlan['virtualGuests']: - vs_table.add_row([vsi['hostname'], - vsi['domain'], + vs_table.add_row([vsi.get('hostname'), + vsi.get('domain'), vsi.get('primaryIpAddress'), vsi.get('primaryBackendIpAddress')]) table.add_row(['vs', vs_table]) @@ -72,8 +72,8 @@ if vlan.get('hardware'): hw_table = formatting.Table(server_columns) for hardware in vlan['hardware']: - hw_table.add_row([hardware['hostname'], - hardware['domain'], + hw_table.add_row([hardware.get('hostname'), + hardware.get('domain'), hardware.get('primaryIpAddress'), hardware.get('primaryBackendIpAddress')]) table.add_row(['hardware', hw_table]) diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/vlan/list.py python-softlayer-5.6.4/SoftLayer/CLI/vlan/list.py --- python-softlayer-5.4.4/SoftLayer/CLI/vlan/list.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/vlan/list.py 2018-11-16 23:06:56.000000000 +0000 @@ -26,8 +26,12 @@ help='Filter by datacenter shortname (sng01, dal05, ...)') @click.option('--number', '-n', help='Filter by VLAN number') @click.option('--name', help='Filter by VLAN name') +@click.option('--limit', '-l', + help='How many results to get in one api call, default is 100', + default=100, + show_default=True) @environment.pass_env -def cli(env, sortby, datacenter, number, name): +def cli(env, sortby, datacenter, number, name, limit): """List VLANs.""" mgr = SoftLayer.NetworkManager(env.client) @@ -37,7 +41,8 @@ vlans = mgr.list_vlans(datacenter=datacenter, vlan_number=number, - name=name) + name=name, + limit=limit) for vlan in vlans: table.add_row([ vlan['id'], diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/subnet/add.py python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/subnet/add.py --- python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/subnet/add.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/subnet/add.py 2018-11-16 23:06:56.000000000 +0000 @@ -18,14 +18,14 @@ type=int, help='Subnet identifier to add') @click.option('-t', - '--type', '--subnet-type', + '--type', required=True, type=click.Choice(['internal', 'remote', 'service']), help='Subnet type to add') @click.option('-n', - '--network', '--network-identifier', + '--network', default=None, type=NetworkParamType(), help='Subnet network identifier to create') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/subnet/remove.py python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/subnet/remove.py --- python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/subnet/remove.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/subnet/remove.py 2018-11-16 23:06:56.000000000 +0000 @@ -16,8 +16,8 @@ type=int, help='Subnet identifier to remove') @click.option('-t', - '--type', '--subnet-type', + '--type', required=True, type=click.Choice(['internal', 'remote', 'service']), help='Subnet type to add') diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/translation/add.py python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/translation/add.py --- python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/translation/add.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/translation/add.py 2018-11-16 23:06:56.000000000 +0000 @@ -11,12 +11,10 @@ @click.command() @click.argument('context_id', type=int) -# todo: Update to utilize custom IP address type @click.option('-s', '--static-ip', required=True, help='Static IP address value') -# todo: Update to utilize custom IP address type @click.option('-r', '--remote-ip', required=True, diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/translation/update.py python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/translation/update.py --- python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/translation/update.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/translation/update.py 2018-11-16 23:06:56.000000000 +0000 @@ -15,12 +15,10 @@ required=True, type=int, help='Translation identifier to update') -# todo: Update to utilize custom IP address type @click.option('-s', '--static-ip', default=None, help='Static IP address value') -# todo: Update to utilize custom IP address type @click.option('-r', '--remote-ip', default=None, diff -Nru python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/update.py python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/update.py --- python-softlayer-5.4.4/SoftLayer/CLI/vpn/ipsec/update.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/CLI/vpn/ipsec/update.py 2018-11-16 23:06:56.000000000 +0000 @@ -13,55 +13,54 @@ @click.option('--friendly-name', default=None, help='Friendly name value') -# todo: Update to utilize custom IP address type @click.option('--remote-peer', default=None, help='Remote peer IP address value') @click.option('--preshared-key', default=None, help='Preshared key value') -@click.option('--p1-auth', - '--phase1-auth', +@click.option('--phase1-auth', + '--p1-auth', default=None, type=click.Choice(['MD5', 'SHA1', 'SHA256']), help='Phase 1 authentication value') -@click.option('--p1-crypto', - '--phase1-crypto', +@click.option('--phase1-crypto', + '--p1-crypto', default=None, type=click.Choice(['DES', '3DES', 'AES128', 'AES192', 'AES256']), help='Phase 1 encryption value') -@click.option('--p1-dh', - '--phase1-dh', +@click.option('--phase1-dh', + '--p1-dh', default=None, type=click.Choice(['0', '1', '2', '5']), help='Phase 1 diffie hellman group value') -@click.option('--p1-key-ttl', - '--phase1-key-ttl', +@click.option('--phase1-key-ttl', + '--p1-key-ttl', default=None, type=click.IntRange(120, 172800), help='Phase 1 key life value') -@click.option('--p2-auth', - '--phase2-auth', +@click.option('--phase2-auth', + '--p2-auth', default=None, type=click.Choice(['MD5', 'SHA1', 'SHA256']), help='Phase 2 authentication value') -@click.option('--p2-crypto', - '--phase2-crypto', +@click.option('--phase2-crypto', + '--p2-crypto', default=None, type=click.Choice(['DES', '3DES', 'AES128', 'AES192', 'AES256']), help='Phase 2 encryption value') -@click.option('--p2-dh', - '--phase2-dh', +@click.option('--phase2-dh', + '--p2-dh', default=None, type=click.Choice(['0', '1', '2', '5']), help='Phase 2 diffie hellman group value') -@click.option('--p2-forward-secrecy', - '--phase2-forward-secrecy', +@click.option('--phase2-forward-secrecy', + '--p2-forward-secrecy', default=None, type=click.IntRange(0, 1), help='Phase 2 perfect forward secrecy value') -@click.option('--p2-key-ttl', - '--phase2-key-ttl', +@click.option('--phase2-key-ttl', + '--p2-key-ttl', default=None, type=click.IntRange(120, 172800), help='Phase 2 key life value') diff -Nru python-softlayer-5.4.4/SoftLayer/consts.py python-softlayer-5.6.4/SoftLayer/consts.py --- python-softlayer-5.4.4/SoftLayer/consts.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/consts.py 2018-11-16 23:06:56.000000000 +0000 @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v5.4.4' +VERSION = 'v5.6.4' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff -Nru python-softlayer-5.4.4/SoftLayer/exceptions.py python-softlayer-5.6.4/SoftLayer/exceptions.py --- python-softlayer-5.4.4/SoftLayer/exceptions.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/exceptions.py 2018-11-16 23:06:56.000000000 +0000 @@ -21,6 +21,7 @@ Provides faultCode and faultString properties. """ + def __init__(self, fault_code, fault_string, *args): SoftLayerError.__init__(self, fault_string, *args) self.faultCode = fault_code diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Account.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Account.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Account.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Account.py 2018-11-16 23:06:56.000000000 +0000 @@ -1,5 +1,6 @@ # -*- coding: UTF-8 -*- +# # pylint: disable=bad-continuation getPrivateBlockDeviceTemplateGroups = [{ 'accountId': 1234, 'blockDevices': [], @@ -315,9 +316,14 @@ { 'id': '100', 'networkIdentifier': '10.0.0.1', + 'cidr': '/24', + 'networkVlanId': 123, 'datacenter': {'name': 'dal00'}, 'version': 4, - 'subnetType': 'PRIMARY' + 'subnetType': 'PRIMARY', + 'ipAddressCount': 10, + 'virtualGuests': [], + 'hardware': [] }] getSshKeys = [{'id': '100', 'label': 'Test 1'}, @@ -553,9 +559,86 @@ 'name': 'dal05' }, 'memoryCapacity': 242, - 'name': 'khnguyendh', + 'name': 'test-dedicated', 'diskCapacity': 1200, 'guestCount': 1, 'cpuCount': 56, - 'id': 44701 + 'id': 12345 }] + + +getUsers = [ + {'displayName': 'ChristopherG', + 'hardwareCount': 138, + 'id': 11100, + 'userStatus': {'name': 'Active'}, + 'username': 'SL1234', + 'virtualGuestCount': 99}, + {'displayName': 'PulseL', + 'hardwareCount': 100, + 'id': 11111, + 'userStatus': {'name': 'Active'}, + 'username': 'sl1234-abob', + 'virtualGuestCount': 99} +] + +getReservedCapacityGroups = [ + { + 'accountId': 1234, + 'backendRouterId': 1411193, + 'createDate': '2018-09-24T16:33:09-06:00', + 'id': 3103, + 'modifyDate': '', + 'name': 'test-capacity', + 'availableInstanceCount': 1, + 'instanceCount': 2, + 'occupiedInstanceCount': 1, + 'backendRouter': { + 'accountId': 1, + 'bareMetalInstanceFlag': 0, + 'domain': 'softlayer.com', + 'fullyQualifiedDomainName': 'bcr02a.dal13.softlayer.com', + 'hardwareStatusId': 5, + 'hostname': 'bcr02a.dal13', + 'id': 1411193, + 'notes': '', + 'provisionDate': '', + 'serviceProviderId': 1, + 'serviceProviderResourceId': '', + 'primaryIpAddress': '10.0.144.28', + 'datacenter': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + 'statusId': 2 + }, + 'hardwareFunction': { + 'code': 'ROUTER', + 'description': 'Router', + 'id': 1 + }, + 'topLevelLocation': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + 'statusId': 2 + } + }, + 'instances': [ + { + 'id': 3501, + 'billingItem': { + 'description': 'B1.1x2 (1 Year Term)', + 'hourlyRecurringFee': '.032' + } + }, + { + 'id': 3519, + 'billingItem': { + 'description': 'B1.1x2 (1 Year Term)', + 'hourlyRecurringFee': '.032' + } + } + ] + } +] diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Event_Log.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Event_Log.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Event_Log.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Event_Log.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,22 @@ +getAllObjects = [ + { + "accountId": 1234, + "eventCreateDate": "2018-05-15T14:37:13.378291-06:00", + "eventName": "Login Successful", + "ipAddress": "1.2.3.4", + "label": "sl1234-aaa", + "metaData": "", + "objectId": 6657767, + "objectName": "User", + "openIdConnectUserName": "a@b.com", + "resource": { + "accountId": 307608, + "address1": "4849 Alpha Rd", + "city": "Dallas" + }, + "traceId": "5afb44f95c61f", + "userId": 6657767, + "userType": "CUSTOMER", + "username": "sl1234-aaa" + } +] diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Network_Pod.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Network_Pod.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Network_Pod.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Network_Pod.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,22 @@ +getAllObjects = [ + { + 'backendRouterId': 117917, + 'backendRouterName': 'bcr01a.ams01', + 'datacenterId': 265592, + 'datacenterLongName': 'Amsterdam 1', + 'datacenterName': 'ams01', + 'frontendRouterId': 117960, + 'frontendRouterName': 'fcr01a.ams01', + 'name': 'ams01.pod01' + }, + { + 'backendRouterId': 1115295, + 'backendRouterName': 'bcr01a.wdc07', + 'datacenterId': 2017603, + 'datacenterLongName': 'Washington 7', + 'datacenterName': 'wdc07', + 'frontendRouterId': 1114993, + 'frontendRouterName': 'fcr01a.wdc07', + 'name': 'wdc07.pod01' + } +] diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Product_Order.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Product_Order.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Product_Order.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Product_Order.py 2018-11-16 23:06:56.000000000 +0000 @@ -14,3 +14,69 @@ 'item': {'id': 1, 'description': 'this is a thing'}, }]} placeOrder = verifyOrder + +# Reserved Capacity Stuff + +rsc_verifyOrder = { + 'orderContainers': [ + { + 'locationObject': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13' + }, + 'name': 'test-capacity', + 'postTaxRecurring': '0.32', + 'prices': [ + { + 'item': { + 'id': 1, + 'description': 'B1.1x2 (1 Year ''Term)', + 'keyName': 'B1_1X2_1_YEAR_TERM', + } + } + ] + } + ], + 'postTaxRecurring': '0.32', +} + +rsc_placeOrder = { + 'orderDate': '2013-08-01 15:23:45', + 'orderId': 1234, + 'orderDetails': { + 'postTaxRecurring': '0.32', + }, + 'placedOrder': { + 'status': 'Great, thanks for asking', + 'locationObject': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13' + }, + 'name': 'test-capacity', + 'items': [ + { + 'description': 'B1.1x2 (1 Year ''Term)', + 'keyName': 'B1_1X2_1_YEAR_TERM', + 'categoryCode': 'guest_core', + } + ] + } +} + +rsi_placeOrder = { + 'orderId': 1234, + 'orderDetails': { + 'prices': [ + { + 'id': 4, + 'item': { + 'id': 1, + 'description': 'B1.1x2 (1 Year ''Term)', + 'keyName': 'B1_1X2_1_YEAR_TERM', + } + } + ] + } +} diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,63 @@ +getObject = { + "id": 405, + "keyName": "AC1_8X60X25", + "prices": [ + { + "hourlyRecurringFee": "1.425", + "id": 207345, + "recurringFee": "936.23", + "item": { + "description": "1 x P100 GPU", + "id": 10933, + "keyName": "1_X_P100_GPU", + "itemCategory": { + "categoryCode": "guest_pcie_device0", + "id": 1259 + } + } + }, + { + "hourlyRecurringFee": "0", + "id": 2202, + "recurringFee": "0", + "item": { + "description": "25 GB (SAN)", + "id": 1178, + "keyName": "GUEST_DISK_25_GB_SAN", + "itemCategory": { + "categoryCode": "guest_disk0", + "id": 81 + } + } + }, + { + "hourlyRecurringFee": ".342", + "id": 207361, + "recurringFee": "224.69", + "item": { + "description": "60 GB", + "id": 10939, + "keyName": "RAM_0_UNIT_PLACEHOLDER_10", + "itemCategory": { + "categoryCode": "ram", + "id": 3 + } + } + }, + { + "hourlyRecurringFee": ".181", + "id": 209595, + "recurringFee": "118.26", + "item": { + "capacity": 8, + "description": "8 x 2.0 GHz or higher Cores", + "id": 11307, + "keyName": "GUEST_CORE_8", + "itemCategory": { + "categoryCode": "guest_core", + "id": 80 + } + } + } + ] +} diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Product_Package.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Product_Package.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Product_Package.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Product_Package.py 2018-11-16 23:06:56.000000000 +0000 @@ -666,7 +666,6 @@ ] } - SAAS_REST_PACKAGE = { 'categories': [ {'categoryCode': 'storage_as_a_service'} @@ -790,134 +789,155 @@ getItems = [ { 'id': 1234, + 'keyName': 'KeyName01', 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, 'prices': [{'id': 1122, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 26, 'name': 'Uplink Port Speeds', 'categoryCode': 'port_speed'}]}], }, { 'id': 2233, + 'keyName': 'KeyName02', 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, 'prices': [{'id': 4477, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 26, 'name': 'Uplink Port Speeds', 'categoryCode': 'port_speed'}]}], }, { 'id': 1239, + 'keyName': 'KeyName03', 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, 'prices': [{'id': 1133, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 3, 'name': 'RAM', 'categoryCode': 'ram'}]}], }, { 'id': 1240, + 'keyName': 'KeyName014', 'capacity': '4', 'units': 'PRIVATE_CORE', 'description': 'Computing Instance (Dedicated)', 'itemCategory': {'categoryCode': 'Computing Instance'}, 'prices': [{'id': 1007, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 80, 'name': 'Computing Instance', 'categoryCode': 'guest_core'}]}], }, { 'id': 1250, + 'keyName': 'KeyName015', 'capacity': '4', 'units': 'CORE', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, 'prices': [{'id': 1144, 'locationGroupId': None, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 80, 'name': 'Computing Instance', 'categoryCode': 'guest_core'}]}], }, { 'id': 112233, + 'keyName': 'KeyName016', 'capacity': '55', 'units': 'CORE', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, 'prices': [{'id': 332211, 'locationGroupId': 1, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 80, 'name': 'Computing Instance', 'categoryCode': 'guest_core'}]}], }, { 'id': 4439, + 'keyName': 'KeyName017', 'capacity': '1', 'description': '1 GB iSCSI Storage', 'itemCategory': {'categoryCode': 'iscsi'}, - 'prices': [{'id': 2222}], + 'prices': [{'id': 2222, 'hourlyRecurringFee': 0.0}], }, { 'id': 1121, + 'keyName': 'KeyName081', 'capacity': '20', 'description': '20 GB iSCSI snapshot', 'itemCategory': {'categoryCode': 'iscsi_snapshot_space'}, - 'prices': [{'id': 2014}], + 'prices': [{'id': 2014, 'hourlyRecurringFee': 0.0}], }, { 'id': 4440, + 'keyName': 'KeyName019', 'capacity': '4', 'description': '4 Portable Public IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_pub'}, - 'prices': [{'id': 4444}], + 'prices': [{'id': 4444, 'hourlyRecurringFee': 0.0}], }, { 'id': 8880, + 'keyName': 'KeyName0199', 'capacity': '8', 'description': '8 Portable Public IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_pub'}, - 'prices': [{'id': 8888}], + 'prices': [{'id': 8888, 'hourlyRecurringFee': 0.0}], }, { 'id': 44400, + 'keyName': 'KeyName0155', 'capacity': '4', 'description': '4 Portable Private IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_priv'}, - 'prices': [{'id': 44441}], + 'prices': [{'id': 44441, 'hourlyRecurringFee': 0.0}], }, { 'id': 88800, + 'keyName': 'KeyName0144', 'capacity': '8', 'description': '8 Portable Private IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_priv'}, - 'prices': [{'id': 88881}], + 'prices': [{'id': 88881, 'hourlyRecurringFee': 0.0}], }, { 'id': 10, + 'keyName': 'KeyName0341', 'capacity': '0', 'description': 'Global IPv4', 'itemCategory': {'categoryCode': 'global_ipv4'}, - 'prices': [{'id': 11}], + 'prices': [{'id': 11, 'hourlyRecurringFee': 0.0}], }, { 'id': 66464, + 'keyName': 'KeyName0211', 'capacity': '64', 'description': '/64 Block Portable Public IPv6 Addresses', 'itemCategory': {'categoryCode': 'static_ipv6_addresses'}, - 'prices': [{'id': 664641}], + 'prices': [{'id': 664641, 'hourlyRecurringFee': 0.0}], }, { 'id': 610, + 'keyName': 'KeyName031', 'capacity': '0', 'description': 'Global IPv6', 'itemCategory': {'categoryCode': 'global_ipv6'}, - 'prices': [{'id': 611}], + 'prices': [{'id': 611, 'hourlyRecurringFee': 0.0}], }] -getItemPrices = [ +getItemPricesISCSI = [ { 'currentPriceFlag': '', 'id': 2152, @@ -1133,12 +1153,14 @@ "bundleItems": [ { "capacity": "1200", + "keyName": "1_4_TB_LOCAL_STORAGE_DEDICATED_HOST_CAPACITY", "categories": [{ "categoryCode": "dedicated_host_disk" }] }, { "capacity": "242", + "keyName": "242_GB_RAM", "categories": [{ "categoryCode": "dedicated_host_ram" }] @@ -1218,6 +1240,110 @@ "description": "Dedicated Host" }] +getAllObjectsDHGpu = [{ + "subDescription": "Dedicated Host", + "name": "Dedicated Host", + "items": [{ + "capacity": "56", + "description": "56 Cores x 360 RAM x 1.2 TB x 2 GPU P100 [encryption enabled]", + "bundleItems": [ + { + "capacity": "1200", + "keyName": "1.2 TB Local Storage (Dedicated Host Capacity)", + "categories": [{ + "categoryCode": "dedicated_host_disk" + }] + }, + { + "capacity": "242", + "keyName": "2_GPU_P100_DEDICATED", + "hardwareGenericComponentModel": { + "capacity": "16", + "id": 849, + "hardwareComponentType": { + "id": 20, + "keyName": "GPU" + } + }, + "categories": [{ + "categoryCode": "dedicated_host_ram" + }] + } + ], + "prices": [ + { + "itemId": 10195, + "setupFee": "0", + "recurringFee": "2099", + "tierMinimumThreshold": "", + "hourlyRecurringFee": "3.164", + "oneTimeFee": "0", + "currentPriceFlag": "", + "id": 200269, + "sort": 0, + "onSaleFlag": "", + "laborFee": "0", + "locationGroupId": "", + "quantity": "" + }, + { + "itemId": 10195, + "setupFee": "0", + "recurringFee": "2161.97", + "tierMinimumThreshold": "", + "hourlyRecurringFee": "3.258", + "oneTimeFee": "0", + "currentPriceFlag": "", + "id": 200271, + "sort": 0, + "onSaleFlag": "", + "laborFee": "0", + "locationGroupId": 503, + "quantity": "" + } + ], + "keyName": "56_CORES_X_484_RAM_X_1_5_TB_X_2_GPU_P100", + "id": 10195, + "itemCategory": { + "categoryCode": "dedicated_virtual_hosts" + } + }], + "keyName": "DEDICATED_HOST", + "unitSize": "", + "regions": [{ + "location": { + "locationPackageDetails": [{ + "isAvailable": 1, + "locationId": 138124, + "packageId": 813 + }], + "location": { + "statusId": 2, + "priceGroups": [{ + "locationGroupTypeId": 82, + "description": "CDN - North America - Akamai", + "locationGroupType": { + "name": "PRICING" + }, + "securityLevelId": "", + "id": 1463, + "name": "NORTH-AMERICA-AKAMAI" + }], + "id": 138124, + "name": "dal05", + "longName": "Dallas 5" + } + }, + "keyname": "DALLAS05", + "description": "DAL05 - Dallas", + "sortOrder": 12 + }], + "firstOrderStepId": "", + "id": 813, + "isActive": 1, + "description": "Dedicated Host" +}] + getRegions = [{ "description": "WDC07 - Washington, DC", "keyname": "WASHINGTON07", @@ -1235,3 +1361,292 @@ }] }] }] + +getItemPrices = [ + { + "hourlyRecurringFee": ".093", + "id": 204015, + "recurringFee": "62", + "categories": [ + { + "categoryCode": "guest_core" + } + ], + "item": { + "description": "4 x 2.0 GHz or higher Cores", + "id": 859, + "keyName": "GUEST_CORES_4", + }, + "pricingLocationGroup": { + "id": 503, + "locations": [ + { + "id": 449610, + "longName": "Montreal 1", + "name": "mon01", + "statusId": 2 + }, + { + "id": 449618, + "longName": "Montreal 2", + "name": "mon02", + "statusId": 2 + }, + { + "id": 448994, + "longName": "Toronto 1", + "name": "tor01", + "statusId": 2 + }, + { + "id": 350993, + "longName": "Toronto 2", + "name": "tor02", + "statusId": 2 + }, + { + "id": 221894, + "longName": "Amsterdam 2", + "name": "ams02", + "statusId": 2 + }, + { + "id": 265592, + "longName": "Amsterdam 1", + "name": "ams01", + "statusId": 2 + }, + { + "id": 814994, + "longName": "Amsterdam 3", + "name": "ams03", + "statusId": 2 + } + ] + } + }, + { + "hourlyRecurringFee": ".006", + "id": 204663, + "recurringFee": "4.1", + "item": { + "description": "100 GB (LOCAL)", + "id": 3899, + "keyName": "GUEST_DISK_100_GB_LOCAL_3", + }, + "pricingLocationGroup": { + "id": 503, + "locations": [ + { + "id": 449610, + "longName": "Montreal 1", + "name": "mon01", + "statusId": 2 + }, + { + "id": 449618, + "longName": "Montreal 2", + "name": "mon02", + "statusId": 2 + }, + { + "id": 448994, + "longName": "Toronto 1", + "name": "tor01", + "statusId": 2 + }, + { + "id": 350993, + "longName": "Toronto 2", + "name": "tor02", + "statusId": 2 + }, + { + "id": 221894, + "longName": "Amsterdam 2", + "name": "ams02", + "statusId": 2 + }, + { + "id": 265592, + "longName": "Amsterdam 1", + "name": "ams01", + "statusId": 2 + }, + { + "id": 814994, + "longName": "Amsterdam 3", + "name": "ams03", + "statusId": 2 + } + ] + } + }, + { + "hourlyRecurringFee": ".217", + "id": 204255, + "recurringFee": "144", + "item": { + "description": "16 GB ", + "id": 1017, + "keyName": "RAM_16_GB", + }, + "pricingLocationGroup": { + "id": 503, + "locations": [ + { + "id": 449610, + "longName": "Montreal 1", + "name": "mon01", + "statusId": 2 + }, + { + "id": 449618, + "longName": "Montreal 2", + "name": "mon02", + "statusId": 2 + }, + { + "id": 448994, + "longName": "Toronto 1", + "name": "tor01", + "statusId": 2 + }, + { + "id": 350993, + "longName": "Toronto 2", + "name": "tor02", + "statusId": 2 + }, + { + "id": 221894, + "longName": "Amsterdam 2", + "name": "ams02", + "statusId": 2 + }, + { + "id": 265592, + "longName": "Amsterdam 1", + "name": "ams01", + "statusId": 2 + }, + { + "id": 814994, + "longName": "Amsterdam 3", + "name": "ams03", + "statusId": 2 + } + ] + } + } +] +getActivePresets = [ + { + "description": "M1.64x512x25", + "id": 799, + "isActive": "1", + "keyName": "M1_64X512X25", + "name": "M1.64x512x25", + "packageId": 835 + }, + { + "description": "M1.56x448x100", + "id": 797, + "isActive": "1", + "keyName": "M1_56X448X100", + "name": "M1.56x448x100", + "packageId": 835 + }, + { + "description": "M1.64x512x100", + "id": 801, + "isActive": "1", + "keyName": "M1_64X512X100", + "name": "M1.64x512x100", + "packageId": 835 + } +] + +getAccountRestrictedActivePresets = [] + +RESERVED_CAPACITY = [{"id": 1059}] +getItems_RESERVED_CAPACITY = [ + { + 'id': 12273, + 'keyName': 'B1_1X2_1_YEAR_TERM', + 'description': 'B1 1x2 1 year term', + 'capacity': 12, + 'itemCategory': { + 'categoryCode': 'reserved_capacity', + 'id': 2060, + 'name': 'Reserved Capacity', + 'quantityLimit': 20, + 'sortOrder': '' + }, + 'prices': [ + { + 'currentPriceFlag': '', + 'hourlyRecurringFee': '.032', + 'id': 217561, + 'itemId': 12273, + 'laborFee': '0', + 'locationGroupId': '', + 'onSaleFlag': '', + 'oneTimeFee': '0', + 'quantity': '', + 'setupFee': '0', + 'sort': 0, + 'tierMinimumThreshold': '', + 'categories': [ + { + 'categoryCode': 'reserved_capacity', + 'id': 2060, + 'name': 'Reserved Capacity', + 'quantityLimit': 20, + 'sortOrder': '' + } + ] + } + ] + } +] + +getItems_1_IPV6_ADDRESS = [ + { + 'id': 4097, + 'keyName': '1_IPV6_ADDRESS', + 'itemCategory': { + 'categoryCode': 'pri_ipv6_addresses', + 'id': 325, + 'name': 'Primary IPv6 Addresses', + 'quantityLimit': 0, + 'sortOrder': 34 + }, + 'prices': [ + { + 'currentPriceFlag': '', + 'hourlyRecurringFee': '0', + 'id': 17129, + 'itemId': 4097, + 'laborFee': '0', + 'locationGroupId': '', + 'onSaleFlag': '', + 'oneTimeFee': '0', + 'quantity': '', + 'recurringFee': '0', + 'setupFee': '0', + 'sort': 0, + 'tierMinimumThreshold': '', + 'categories': [ + { + 'categoryCode': 'pri_ipv6_addresses', + 'id': 325, + 'name': 'Primary IPv6 Addresses', + 'quantityLimit': 0, + 'sortOrder': 34 + } + ] + } + ] + } +] diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py 2018-11-16 23:06:56.000000000 +0000 @@ -6,3 +6,4 @@ 'notes': 'notes', 'key': 'ssh-rsa AAAAB3N...pa67 user@example.com'} createObject = getObject +getAllObjects = [getObject] diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_User_Customer_CustomerPermission_Permission.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_User_Customer_CustomerPermission_Permission.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_User_Customer_CustomerPermission_Permission.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_User_Customer_CustomerPermission_Permission.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,42 @@ +getAllObjects = [ + { + "key": "T_1", + "keyName": "TICKET_VIEW", + "name": "View Tickets" + }, + { + "key": "T_2", + "keyName": "TEST", + "name": "A Testing Permission" + }, + { + "key": "T_3", + "keyName": "TEST_3", + "name": "A Testing Permission 3" + }, + { + "key": "T_4", + "keyName": "TEST_4", + "name": "A Testing Permission 4" + }, + { + "key": "T_5", + "keyName": "ACCESS_ALL_HARDWARE", + "name": "A Testing Permission 5" + }, + { + 'key': 'ALL_1', + 'keyName': 'ACCESS_ALL_HARDWARE', + 'name': 'All Hardware Access' + }, + { + 'key': 'A_1', + 'keyName': 'ACCOUNT_SUMMARY_VIEW', + 'name': 'View Account Summary' + }, + { + 'key': 'A_10', + 'keyName': 'ADD_SERVICE_STORAGE', + 'name': 'Add/Upgrade Storage (StorageLayer)' + } +] diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_User_Customer.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_User_Customer.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_User_Customer.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_User_Customer.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,81 @@ +getObject = { + 'accountId': 12345, + 'address1': '315 Test Street', + 'apiAuthenticationKeys': [{'authenticationKey': 'aaaaaaaaaaaaaaaaaaaaaaaaa'}], + 'city': 'Houston', + 'companyName': 'SoftLayer Development Community', + 'country': 'US', + 'createDate': '2014-08-18T12:58:02-06:00', + 'displayName': 'Test', + 'email': 'test@us.ibm.com', + 'firstName': 'Test', + 'id': 244956, + 'isMasterUserFlag': False, + 'lastName': 'Testerson', + 'openIdConnectUserName': 'test@us.ibm.com', + 'parent': {'id': 167758, 'username': 'SL12345'}, + 'parentId': 167758, + 'postalCode': '77002', + 'pptpVpnAllowedFlag': False, + 'sslVpnAllowedFlag': True, + 'state': 'TX', + 'statusDate': None, + 'successfulLogins': [ + {'createDate': '2018-05-08T15:28:32-06:00', + 'ipAddress': '175.125.126.118', + 'successFlag': True, + 'userId': 244956}, + ], + 'timezone': { + 'id': 113, + 'longName': '(GMT-06:00) America/Chicago - CST', + 'name': 'America/Chicago', + 'offset': '-0600', + 'shortName': 'CST'}, + 'timezoneId': 113, + 'unsuccessfulLogins': [ + {'createDate': '2018-02-09T14:13:15-06:00', + 'ipAddress': '73.136.219.36', + 'successFlag': False, + 'userId': 244956}, + ], + 'userStatus': {'name': 'Active'}, + 'userStatusId': 1001, + 'username': 'SL12345-test', + 'vpnManualConfig': False, + 'permissions': [ + {'key': 'ALL_1', + 'keyName': 'ACCESS_ALL_HARDWARE', + 'name': 'All Hardware Access'} + ], + 'roles': [] +} + +getPermissions = [ + {'key': 'ALL_1', + 'keyName': 'ACCESS_ALL_HARDWARE', + 'name': 'All Hardware Access'}, + {'key': 'A_1', + 'keyName': 'ACCOUNT_SUMMARY_VIEW', + 'name': 'View Account Summary'}, + {'key': 'A_10', + 'keyName': 'ADD_SERVICE_STORAGE', + 'name': 'Add/Upgrade Storage (StorageLayer)'} +] + + +getLoginAttempts = [ + { + "createDate": "2017-10-03T09:28:33-06:00", + "ipAddress": "1.2.3.4", + "successFlag": False, + "userId": 1111, + "username": "sl1234" + } +] + +addBulkPortalPermission = True +removeBulkPortalPermission = True +createObject = getObject +editObject = True +addApiAuthenticationKey = True diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py 2018-11-16 23:06:56.000000000 +0000 @@ -11,57 +11,57 @@ getAvailableRouters = [ - {'hostname': 'bcr01a.dal05', 'id': 51218}, - {'hostname': 'bcr02a.dal05', 'id': 83361}, - {'hostname': 'bcr03a.dal05', 'id': 122762}, - {'hostname': 'bcr04a.dal05', 'id': 147566} + {'hostname': 'bcr01a.dal05', 'id': 12345}, + {'hostname': 'bcr02a.dal05', 'id': 12346}, + {'hostname': 'bcr03a.dal05', 'id': 12347}, + {'hostname': 'bcr04a.dal05', 'id': 12348} ] getObjectById = { 'datacenter': { - 'id': 138124, + 'id': 12345, 'name': 'dal05', 'longName': 'Dallas 5' }, 'memoryCapacity': 242, 'modifyDate': '2017-11-06T11:38:20-06:00', - 'name': 'khnguyendh', + 'name': 'test-dedicated', 'diskCapacity': 1200, 'backendRouter': { - 'domain': 'softlayer.com', + 'domain': 'test.com', 'hostname': 'bcr01a.dal05', - 'id': 51218 + 'id': 12345 }, 'guestCount': 1, 'cpuCount': 56, 'guests': [{ - 'domain': 'Softlayer.com', - 'hostname': 'khnguyenDHI', - 'id': 43546081, - 'uuid': '806a56ec-0383-4c2e-e6a9-7dc89c4b29a2' + 'domain': 'test.com', + 'hostname': 'test-dedicated', + 'id': 12345, + 'uuid': 'F9329795-4220-4B0A-B970-C86B950667FA' }], 'billingItem': { 'nextInvoiceTotalRecurringAmount': 1515.556, 'orderItem': { - 'id': 263060473, + 'id': 12345, 'order': { 'status': 'APPROVED', 'privateCloudOrderFlag': False, 'modifyDate': '2017-11-02T11:42:50-07:00', 'orderQuoteId': '', - 'userRecordId': 6908745, + 'userRecordId': 12345, 'createDate': '2017-11-02T11:40:56-07:00', 'impersonatingUserRecordId': '', 'orderTypeId': 7, 'presaleEventId': '', 'userRecord': { - 'username': '232298_khuong' + 'username': 'test-dedicated' }, - 'id': 20093269, - 'accountId': 232298 + 'id': 12345, + 'accountId': 12345 } }, - 'id': 235379377, + 'id': 12345, 'children': [ { 'nextInvoiceTotalRecurringAmount': 0.0, @@ -73,6 +73,62 @@ } ] }, - 'id': 44701, + 'id': 12345, 'createDate': '2017-11-02T11:40:56-07:00' } + +deleteObject = True + +getGuests = [{ + 'id': 200, + 'hostname': 'vs-test1', + 'domain': 'test.sftlyr.ws', + 'fullyQualifiedDomainName': 'vs-test1.test.sftlyr.ws', + 'status': {'keyName': 'ACTIVE', 'name': 'Active'}, + 'datacenter': {'id': 50, 'name': 'TEST00', + 'description': 'Test Data Center'}, + 'powerState': {'keyName': 'RUNNING', 'name': 'Running'}, + 'maxCpu': 2, + 'maxMemory': 1024, + 'primaryIpAddress': '172.16.240.2', + 'globalIdentifier': '1a2b3c-1701', + 'primaryBackendIpAddress': '10.45.19.37', + 'hourlyBillingFlag': False, + 'billingItem': { + 'id': 6327, + 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + }, +}, { + 'id': 202, + 'hostname': 'vs-test2', + 'domain': 'test.sftlyr.ws', + 'fullyQualifiedDomainName': 'vs-test2.test.sftlyr.ws', + 'status': {'keyName': 'ACTIVE', 'name': 'Active'}, + 'datacenter': {'id': 50, 'name': 'TEST00', + 'description': 'Test Data Center'}, + 'powerState': {'keyName': 'RUNNING', 'name': 'Running'}, + 'maxCpu': 4, + 'maxMemory': 4096, + 'primaryIpAddress': '172.16.240.7', + 'globalIdentifier': '05a8ac-6abf0', + 'primaryBackendIpAddress': '10.45.19.35', + 'hourlyBillingFlag': True, + 'billingItem': { + 'id': 6327, + 'recurringFee': 1.54, + 'orderItem': { + 'order': { + 'userRecord': { + 'username': 'chechu', + } + } + } + } +}] diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py 2018-11-16 23:06:56.000000000 +0000 @@ -29,5 +29,11 @@ 'id': 100, 'name': 'test_image', }] - +createFromIcos = [{ + 'createDate': '2013-12-05T21:53:03-06:00', + 'globalIdentifier': '0B5DEAF4-643D-46CA-A695-CECBE8832C9D', + 'id': 100, + 'name': 'test_image', +}] copyToExternalSource = True +copyToIcos = True diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py 2018-11-16 23:06:56.000000000 +0000 @@ -14,6 +14,10 @@ {'nextInvoiceTotalRecurringAmount': 1}, {'nextInvoiceTotalRecurringAmount': 1}, ], + 'package': { + "id": 835, + "keyName": "PUBLIC_CLOUD_SERVER" + }, 'orderItem': { 'order': { 'userRecord': { @@ -419,7 +423,113 @@ setPublicNetworkInterfaceSpeed = True createObject = getObject createObjects = [getObject] -generateOrderTemplate = {} +generateOrderTemplate = { + "imageTemplateId": None, + "location": "1854895", + "packageId": 835, + "presetId": 405, + "prices": [ + { + "hourlyRecurringFee": "0", + "id": 45466, + "recurringFee": "0", + "item": { + "description": "CentOS 7.x - Minimal Install (64 bit)" + } + }, + { + "hourlyRecurringFee": "0", + "id": 2202, + "recurringFee": "0", + "item": { + "description": "25 GB (SAN)" + } + }, + { + "hourlyRecurringFee": "0", + "id": 905, + "recurringFee": "0", + "item": { + "description": "Reboot / Remote Console" + } + }, + { + "hourlyRecurringFee": ".02", + "id": 899, + "recurringFee": "10", + "item": { + "description": "1 Gbps Private Network Uplink" + } + }, + { + "hourlyRecurringFee": "0", + "id": 1800, + "item": { + "description": "0 GB Bandwidth Allotment" + } + }, + { + "hourlyRecurringFee": "0", + "id": 21, + "recurringFee": "0", + "item": { + "description": "1 IP Address" + } + }, + { + "hourlyRecurringFee": "0", + "id": 55, + "recurringFee": "0", + "item": { + "description": "Host Ping" + } + }, + { + "hourlyRecurringFee": "0", + "id": 57, + "recurringFee": "0", + "item": { + "description": "Email and Ticket" + } + }, + { + "hourlyRecurringFee": "0", + "id": 58, + "recurringFee": "0", + "item": { + "description": "Automated Notification" + } + }, + { + "hourlyRecurringFee": "0", + "id": 420, + "recurringFee": "0", + "item": { + "description": "Unlimited SSL VPN Users & 1 PPTP VPN User per account" + } + }, + { + "hourlyRecurringFee": "0", + "id": 418, + "recurringFee": "0", + "item": { + "description": "Nessus Vulnerability Assessment & Reporting" + } + } + ], + "quantity": 1, + "sourceVirtualGuestId": None, + "sshKeys": [], + "useHourlyPricing": True, + "virtualGuests": [ + { + "domain": "test.local", + "hostname": "test" + } + ], + "complexType": "SoftLayer_Container_Product_Order_Virtual_Guest" +} + setUserMetadata = ['meta'] reloadOperatingSystem = 'OK' setTags = True diff -Nru python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py --- python-softlayer-5.4.4/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,85 @@ +getObject = { + 'accountId': 1234, + 'backendRouterId': 1411193, + 'backendRouter': { + 'fullyQualifiedDomainName': 'bcr02a.dal13.softlayer.com', + 'hostname': 'bcr02a.dal13', + 'id': 1411193, + 'datacenter': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + + } + }, + 'createDate': '2018-09-24T16:33:09-06:00', + 'id': 3103, + 'modifyDate': '', + 'name': 'test-capacity', + 'instances': [ + { + 'createDate': '2018-09-24T16:33:09-06:00', + 'guestId': 62159257, + 'id': 3501, + 'billingItem': { + 'id': 348319479, + 'recurringFee': '3.04', + 'category': {'name': 'Reserved Capacity'}, + 'item': { + 'keyName': 'B1_1X2_1_YEAR_TERM' + } + }, + 'guest': { + 'domain': 'cgallo.com', + 'hostname': 'test-reserved-instance', + 'id': 62159257, + 'modifyDate': '2018-09-27T16:49:26-06:00', + 'primaryBackendIpAddress': '10.73.150.179', + 'primaryIpAddress': '169.62.147.165' + } + }, + { + 'createDate': '2018-09-24T16:33:10-06:00', + 'guestId': 62159275, + 'id': 3519, + 'billingItem': { + 'id': 348319443, + 'recurringFee': '3.04', + 'category': { + 'name': 'Reserved Capacity' + }, + 'item': { + 'keyName': 'B1_1X2_1_YEAR_TERM' + } + } + } + ] +} + + +getObject_pending = { + 'accountId': 1234, + 'backendRouterId': 1411193, + 'backendRouter': { + 'fullyQualifiedDomainName': 'bcr02a.dal13.softlayer.com', + 'hostname': 'bcr02a.dal13', + 'id': 1411193, + 'datacenter': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + + } + }, + 'createDate': '2018-09-24T16:33:09-06:00', + 'id': 3103, + 'modifyDate': '', + 'name': 'test-capacity', + 'instances': [ + { + 'createDate': '2018-09-24T16:33:09-06:00', + 'guestId': 62159257, + 'id': 3501, + } + ] +} diff -Nru python-softlayer-5.4.4/SoftLayer/managers/block.py python-softlayer-5.6.4/SoftLayer/managers/block.py --- python-softlayer-5.4.4/SoftLayer/managers/block.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/block.py 2018-11-16 23:06:56.000000000 +0000 @@ -144,7 +144,7 @@ 'hourlySchedule', 'dailySchedule', 'weeklySchedule' - ] + ] kwargs['mask'] = ','.join(items) @@ -510,6 +510,10 @@ block_volume = self.get_block_volume_details( volume_id, mask='mask[id,billingItem[id,hourlyFlag]]') + + if 'billingItem' not in block_volume: + raise exceptions.SoftLayerError("Block Storage was already cancelled") + billing_item_id = block_volume['billingItem']['id'] if utils.lookup(block_volume, 'billingItem', 'hourlyFlag'): diff -Nru python-softlayer-5.4.4/SoftLayer/managers/dedicated_host.py python-softlayer-5.6.4/SoftLayer/managers/dedicated_host.py --- python-softlayer-5.4.4/SoftLayer/managers/dedicated_host.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/dedicated_host.py 2018-11-16 23:06:56.000000000 +0000 @@ -33,10 +33,144 @@ self.client = client self.account = client['Account'] self.host = client['Virtual_DedicatedHost'] + self.guest = client['Virtual_Guest'] if ordering_manager is None: self.ordering_manager = ordering.OrderingManager(client) + def cancel_host(self, host_id): + """Cancel a dedicated host immediately, it fails if there are still guests in the host. + + :param host_id: The ID of the dedicated host to be cancelled. + :return: True on success or an exception + + Example:: + # Cancels dedicated host id 12345 + result = mgr.cancel_host(12345) + + """ + return self.host.deleteObject(id=host_id) + + def cancel_guests(self, host_id): + """Cancel all guests into the dedicated host immediately. + + To cancel an specified guest use the method VSManager.cancel_instance() + + :param host_id: The ID of the dedicated host. + :return: The id, fqdn and status of all guests into a dictionary. The status + could be 'Cancelled' or an exception message, The dictionary is empty + if there isn't any guest in the dedicated host. + + Example:: + # Cancel guests of dedicated host id 12345 + result = mgr.cancel_guests(12345) + """ + result = [] + + guests = self.host.getGuests(id=host_id, mask='id,fullyQualifiedDomainName') + + if guests: + for vs in guests: + status_info = { + 'id': vs['id'], + 'fqdn': vs['fullyQualifiedDomainName'], + 'status': self._delete_guest(vs['id']) + } + result.append(status_info) + + return result + + def list_guests(self, host_id, tags=None, cpus=None, memory=None, hostname=None, + domain=None, local_disk=None, nic_speed=None, public_ip=None, + private_ip=None, **kwargs): + """Retrieve a list of all virtual servers on the dedicated host. + + Example:: + + # Print out a list of instances with 4 cpu cores in the host id 12345. + + for vsi in mgr.list_guests(host_id=12345, cpus=4): + print vsi['fullyQualifiedDomainName'], vsi['primaryIpAddress'] + + # Using a custom object-mask. Will get ONLY what is specified + object_mask = "mask[hostname,monitoringRobot[robotStatus]]" + for vsi in mgr.list_guests(mask=object_mask,cpus=4): + print vsi + + :param integer host_id: the identifier of dedicated host + :param list tags: filter based on list of tags + :param integer cpus: filter based on number of CPUS + :param integer memory: filter based on amount of memory + :param string hostname: filter based on hostname + :param string domain: filter based on domain + :param string local_disk: filter based on local_disk + :param integer nic_speed: filter based on network speed (in MBPS) + :param string public_ip: filter based on public ip address + :param string private_ip: filter based on private ip address + :param dict \\*\\*kwargs: response-level options (mask, limit, etc.) + :returns: Returns a list of dictionaries representing the matching + virtual servers + """ + if 'mask' not in kwargs: + items = [ + 'id', + 'globalIdentifier', + 'hostname', + 'domain', + 'fullyQualifiedDomainName', + 'primaryBackendIpAddress', + 'primaryIpAddress', + 'lastKnownPowerState.name', + 'hourlyBillingFlag', + 'powerState', + 'maxCpu', + 'maxMemory', + 'datacenter', + 'activeTransaction.transactionStatus[friendlyName,name]', + 'status', + ] + kwargs['mask'] = "mask[%s]" % ','.join(items) + + _filter = utils.NestedDict(kwargs.get('filter') or {}) + + if tags: + _filter['guests']['tagReferences']['tag']['name'] = { + 'operation': 'in', + 'options': [{'name': 'data', 'value': tags}], + } + + if cpus: + _filter['guests']['maxCpu'] = utils.query_filter(cpus) + + if memory: + _filter['guests']['maxMemory'] = utils.query_filter(memory) + + if hostname: + _filter['guests']['hostname'] = utils.query_filter(hostname) + + if domain: + _filter['guests']['domain'] = utils.query_filter(domain) + + if local_disk is not None: + _filter['guests']['localDiskFlag'] = ( + utils.query_filter(bool(local_disk))) + + if nic_speed: + _filter['guests']['networkComponents']['maxSpeed'] = ( + utils.query_filter(nic_speed)) + + if public_ip: + _filter['guests']['primaryIpAddress'] = ( + utils.query_filter(public_ip)) + + if private_ip: + _filter['guests']['primaryBackendIpAddress'] = ( + utils.query_filter(private_ip)) + + kwargs['filter'] = _filter.to_dict() + kwargs['iter'] = True + return self.host.getGuests(id=host_id, **kwargs) + def list_instances(self, tags=None, cpus=None, memory=None, hostname=None, disk=None, datacenter=None, **kwargs): """Retrieve a list of all dedicated hosts on the account @@ -243,7 +377,8 @@ capacity, keyName, itemCategory[categoryCode], - bundleItems[capacity, categories[categoryCode]] + bundleItems[capacity,keyName,categories[categoryCode],hardwareGenericComponentModel[id, + hardwareComponentType[keyName]]] ], regions[location[location[priceGroups]]] ''' @@ -317,6 +452,32 @@ if category['categoryCode'] == 'dedicated_host_disk': disk_capacity = capacity['capacity'] + for hardwareComponent in item['bundleItems']: + if hardwareComponent['keyName'].find("GPU") != -1: + hardwareComponentType = hardwareComponent['hardwareGenericComponentModel']['hardwareComponentType'] + gpuComponents = [ + { + 'hardwareComponentModel': { + 'hardwareGenericComponentModel': { + 'id': hardwareComponent['hardwareGenericComponentModel']['id'], + 'hardwareComponentType': { + 'keyName': hardwareComponentType['keyName'] + } + } + } + }, + { + 'hardwareComponentModel': { + 'hardwareGenericComponentModel': { + 'id': hardwareComponent['hardwareGenericComponentModel']['id'], + 'hardwareComponentType': { + 'keyName': hardwareComponentType['keyName'] + } + } + } + } + ] + if locations is not None: for location in locations: if location['locationId'] is not None: @@ -329,6 +490,8 @@ 'id': loc_id } } + if item['keyName'].find("GPU") != -1: + host['pciDevices'] = gpuComponents routers = self.host.getAvailableRouters(host, mask=mask) return routers @@ -355,3 +518,13 @@ item = self._get_item(package, flavor) return self._get_backend_router(location['location']['locationPackageDetails'], item) + + def _delete_guest(self, guest_id): + """Deletes a guest and returns 'Cancelled' or and Exception message""" + msg = 'Cancelled' + try: + self.guest.deleteObject(id=guest_id) + except SoftLayer.SoftLayerAPIError as e: + msg = 'Exception: ' + e.faultString + + return msg diff -Nru python-softlayer-5.4.4/SoftLayer/managers/dns.py python-softlayer-5.6.4/SoftLayer/managers/dns.py --- python-softlayer-5.4.4/SoftLayer/managers/dns.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/dns.py 2018-11-16 23:06:56.000000000 +0000 @@ -89,17 +89,81 @@ :param integer id: the zone's ID :param record: the name of the record to add - :param record_type: the type of record (A, AAAA, CNAME, MX, TXT, etc.) + :param record_type: the type of record (A, AAAA, CNAME, TXT, etc.) :param data: the record's value :param integer ttl: the TTL or time-to-live value (default: 60) """ - return self.record.createObject({ - 'domainId': zone_id, - 'ttl': ttl, + resource_record = self._generate_create_dict(record, record_type, data, + ttl, domainId=zone_id) + return self.record.createObject(resource_record) + + def create_record_mx(self, zone_id, record, data, ttl=60, priority=10): + """Create a mx resource record on a domain. + + :param integer id: the zone's ID + :param record: the name of the record to add + :param data: the record's value + :param integer ttl: the TTL or time-to-live value (default: 60) + :param integer priority: the priority of the target host + + """ + resource_record = self._generate_create_dict(record, 'MX', data, ttl, + domainId=zone_id, mxPriority=priority) + return self.record.createObject(resource_record) + + def create_record_srv(self, zone_id, record, data, protocol, port, service, + ttl=60, priority=20, weight=10): + """Create a resource record on a domain. + + :param integer id: the zone's ID + :param record: the name of the record to add + :param data: the record's value + :param string protocol: the protocol of the service, usually either TCP or UDP. + :param integer port: the TCP or UDP port on which the service is to be found. + :param string service: the symbolic name of the desired service. + :param integer ttl: the TTL or time-to-live value (default: 60) + :param integer priority: the priority of the target host (default: 20) + :param integer weight: relative weight for records with same priority (default: 10) + + """ + resource_record = self._generate_create_dict(record, 'SRV', data, ttl, domainId=zone_id, + priority=priority, protocol=protocol, port=port, + service=service, weight=weight) + + # The createObject won't creates SRV records unless we send the following complexType. + resource_record['complexType'] = 'SoftLayer_Dns_Domain_ResourceRecord_SrvType' + + return self.record.createObject(resource_record) + + def create_record_ptr(self, record, data, ttl=60): + """Create a reverse record. + + :param record: the public ip address of device for which you would like to manage reverse DNS. + :param data: the record's value + :param integer ttl: the TTL or time-to-live value (default: 60) + + """ + resource_record = self._generate_create_dict(record, 'PTR', data, ttl) + + return self.record.createObject(resource_record) + + @staticmethod + def _generate_create_dict(record, record_type, data, ttl, **kwargs): + """Returns a dict appropriate to pass into Dns_Domain_ResourceRecord::createObject""" + + # Basic dns record structure + resource_record = { 'host': record, - 'type': record_type, - 'data': data}) + 'data': data, + 'ttl': ttl, + 'type': record_type + } + + for (key, value) in kwargs.items(): + resource_record.setdefault(key, value) + + return resource_record def delete_record(self, record_id): """Delete a resource record by its ID. diff -Nru python-softlayer-5.4.4/SoftLayer/managers/file.py python-softlayer-5.6.4/SoftLayer/managers/file.py --- python-softlayer-5.4.4/SoftLayer/managers/file.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/file.py 2018-11-16 23:06:56.000000000 +0000 @@ -142,7 +142,7 @@ 'hourlySchedule', 'dailySchedule', 'weeklySchedule' - ] + ] kwargs['mask'] = ','.join(items) return self.client.call('Network_Storage', 'getSnapshots', diff -Nru python-softlayer-5.4.4/SoftLayer/managers/hardware.py python-softlayer-5.6.4/SoftLayer/managers/hardware.py --- python-softlayer-5.4.4/SoftLayer/managers/hardware.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/hardware.py 2018-11-16 23:06:56.000000000 +0000 @@ -47,6 +47,7 @@ If none is provided, one will be auto initialized. """ + def __init__(self, client, ordering_manager=None): self.client = client self.hardware = self.client['Hardware_Server'] @@ -91,7 +92,7 @@ billing_id = hw_billing['billingItem']['id'] if immediate and not hw_billing['hourlyBillingFlag']: - LOGGER.warning("Immediate cancelation of montly servers is not guaranteed. " + + LOGGER.warning("Immediate cancelation of montly servers is not guaranteed." "Please check the cancelation ticket for updates.") result = self.client.call('Billing_Item', 'cancelItem', @@ -196,7 +197,8 @@ utils.query_filter(private_ip)) kwargs['filter'] = _filter.to_dict() - return self.account.getHardware(**kwargs) + kwargs['iter'] = True + return self.client.call('Account', 'getHardware', **kwargs) @retry(logger=LOGGER) def get_hardware(self, hardware_id, **kwargs): @@ -243,6 +245,12 @@ version, referenceCode]], passwords[username,password]],''' + '''softwareComponents[ + softwareLicense[softwareDescription[manufacturer, + name, + version, + referenceCode]], + passwords[username,password]],''' 'billingItem[' 'id,nextInvoiceTotalRecurringAmount,' 'children[nextInvoiceTotalRecurringAmount],' @@ -260,7 +268,7 @@ """Perform an OS reload of a server with its current configuration. :param integer hardware_id: the instance ID to reload - :param string post_url: The URI of the post-install script to run + :param string post_uri: The URI of the post-install script to run after reload :param list ssh_keys: The SSH keys to add to the root user """ diff -Nru python-softlayer-5.4.4/SoftLayer/managers/image.py python-softlayer-5.6.4/SoftLayer/managers/image.py --- python-softlayer-5.4.4/SoftLayer/managers/image.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/image.py 2018-11-16 23:06:56.000000000 +0000 @@ -16,7 +16,7 @@ """Manages SoftLayer server images. See product information here: - https://knowledgelayer.softlayer.com/topic/image-templates + https://console.bluemix.net/docs/infrastructure/image-templates/image_index.html :param SoftLayer.API.BaseClient client: the client instance """ @@ -120,28 +120,68 @@ return bool(name or note or tag) - def import_image_from_uri(self, name, uri, os_code=None, note=None): + def import_image_from_uri(self, name, uri, os_code=None, note=None, + ibm_api_key=None, root_key_id=None, + wrapped_dek=None, kp_id=None, cloud_init=False, + byol=False, is_encrypted=False): """Import a new image from object storage. :param string name: Name of the new image :param string uri: The URI for an object storage object (.vhd/.iso file) of the format: swift://@// + or (.vhd/.iso/.raw file) of the format: + cos://// if using IBM Cloud + Object Storage :param string os_code: The reference code of the operating system :param string note: Note to add to the image - """ - return self.vgbdtg.createFromExternalSource({ - 'name': name, - 'note': note, - 'operatingSystemReferenceCode': os_code, - 'uri': uri, - }) + :param string ibm_api_key: Ibm Api Key needed to communicate with ICOS + and Key Protect + :param string root_key_id: ID of the root key in Key Protect + :param string wrapped_dek: Wrapped Data Encryption Key provided by + IBM KeyProtect + :param string kp_id: ID of the IBM Key Protect Instance + :param boolean cloud_init: Specifies if image is cloud-init + :param boolean byol: Specifies if image is bring your own license + :param boolean is_encrypted: Specifies if image is encrypted + """ + if 'cos://' in uri: + return self.vgbdtg.createFromIcos({ + 'name': name, + 'note': note, + 'operatingSystemReferenceCode': os_code, + 'uri': uri, + 'ibmApiKey': ibm_api_key, + 'rootKeyId': root_key_id, + 'wrappedDek': wrapped_dek, + 'keyProtectId': kp_id, + 'cloudInit': cloud_init, + 'byol': byol, + 'isEncrypted': is_encrypted + }) + else: + return self.vgbdtg.createFromExternalSource({ + 'name': name, + 'note': note, + 'operatingSystemReferenceCode': os_code, + 'uri': uri, + }) - def export_image_to_uri(self, image_id, uri): + def export_image_to_uri(self, image_id, uri, ibm_api_key=None): """Export image into the given object storage :param int image_id: The ID of the image :param string uri: The URI for object storage of the format swift://@// - """ - return self.vgbdtg.copyToExternalSource({'uri': uri}, id=image_id) + or cos://// if using IBM Cloud + Object Storage + :param string ibm_api_key: Ibm Api Key needed to communicate with IBM + Cloud Object Storage + """ + if 'cos://' in uri: + return self.vgbdtg.copyToIcos({ + 'uri': uri, + 'ibmApiKey': ibm_api_key + }, id=image_id) + else: + return self.vgbdtg.copyToExternalSource({'uri': uri}, id=image_id) diff -Nru python-softlayer-5.4.4/SoftLayer/managers/__init__.py python-softlayer-5.6.4/SoftLayer/managers/__init__.py --- python-softlayer-5.4.4/SoftLayer/managers/__init__.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/__init__.py 2018-11-16 23:06:56.000000000 +0000 @@ -25,10 +25,14 @@ from SoftLayer.managers.sshkey import SshKeyManager from SoftLayer.managers.ssl import SSLManager from SoftLayer.managers.ticket import TicketManager +from SoftLayer.managers.user import UserManager from SoftLayer.managers.vs import VSManager +from SoftLayer.managers.vs_capacity import CapacityManager + __all__ = [ 'BlockStorageManager', + 'CapacityManager', 'CDNManager', 'DedicatedHostManager', 'DNSManager', @@ -46,5 +50,6 @@ 'SshKeyManager', 'SSLManager', 'TicketManager', + 'UserManager', 'VSManager', ] diff -Nru python-softlayer-5.4.4/SoftLayer/managers/ipsec.py python-softlayer-5.6.4/SoftLayer/managers/ipsec.py --- python-softlayer-5.4.4/SoftLayer/managers/ipsec.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/ipsec.py 2018-11-16 23:06:56.000000000 +0000 @@ -227,10 +227,6 @@ translation.pop('customerIpAddressId', None) if notes is not None: translation['notes'] = notes - # todo: Update this signature to return the updated translation - # once internal and customer IP addresses can be fetched - # and set on the translation object, i.e. that which is - # currently being handled in get_translations self.context.editAddressTranslation(translation, id=context_id) return True diff -Nru python-softlayer-5.4.4/SoftLayer/managers/messaging.py python-softlayer-5.6.4/SoftLayer/managers/messaging.py --- python-softlayer-5.4.4/SoftLayer/managers/messaging.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/messaging.py 2018-11-16 23:06:56.000000000 +0000 @@ -30,6 +30,7 @@ :param api_key: SoftLayer API Key :param auth_token: (optional) Starting auth token """ + def __init__(self, endpoint, username, api_key, auth_token=None): self.endpoint = endpoint self.username = username @@ -79,6 +80,7 @@ :param SoftLayer.API.BaseClient client: the client instance """ + def __init__(self, client): self.client = client @@ -152,6 +154,7 @@ :param account_id: Message Queue Account id :param endpoint: Endpoint URL """ + def __init__(self, account_id, endpoint=None): self.account_id = account_id self.endpoint = endpoint diff -Nru python-softlayer-5.4.4/SoftLayer/managers/network.py python-softlayer-5.6.4/SoftLayer/managers/network.py --- python-softlayer-5.4.4/SoftLayer/managers/network.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/network.py 2018-11-16 23:06:56.000000000 +0000 @@ -43,6 +43,7 @@ :param SoftLayer.API.BaseClient client: the client instance """ + def __init__(self, client): self.client = client self.account = client['Account'] @@ -371,7 +372,7 @@ 'description,' '''rules[id, remoteIp, remoteGroupId, direction, ethertype, portRangeMin, - portRangeMax, protocol],''' + portRangeMax, protocol, createDate, modifyDate],''' '''networkComponentBindings[ networkComponent[ id, @@ -476,11 +477,10 @@ utils.query_filter(network_space)) kwargs['filter'] = _filter.to_dict() + kwargs['iter'] = True + return self.client.call('Account', 'getSubnets', **kwargs) - return self.account.getSubnets(**kwargs) - - def list_vlans(self, datacenter=None, vlan_number=None, name=None, - **kwargs): + def list_vlans(self, datacenter=None, vlan_number=None, name=None, **kwargs): """Display a list of all VLANs on the account. This provides a quick overview of all VLANs including information about @@ -513,10 +513,12 @@ if 'mask' not in kwargs: kwargs['mask'] = DEFAULT_VLAN_MASK + kwargs['iter'] = True return self.account.getNetworkVlans(**kwargs) def list_securitygroups(self, **kwargs): """List security groups.""" + kwargs['iter'] = True return self.security_group.getAllObjects(**kwargs) def list_securitygroup_rules(self, group_id): @@ -524,7 +526,7 @@ :param int group_id: The security group to list rules for """ - return self.security_group.getRules(id=group_id) + return self.security_group.getRules(id=group_id, iter=True) def remove_securitygroup_rule(self, group_id, rule_id): """Remove a rule from a security group. diff -Nru python-softlayer-5.4.4/SoftLayer/managers/ordering.py python-softlayer-5.6.4/SoftLayer/managers/ordering.py --- python-softlayer-5.4.4/SoftLayer/managers/ordering.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/ordering.py 2018-11-16 23:06:56.000000000 +0000 @@ -34,6 +34,7 @@ self.package_svc = client['Product_Package'] self.order_svc = client['Product_Order'] self.billing_svc = client['Billing_Order'] + self.package_preset = client['Product_Package_Preset'] def get_packages_of_type(self, package_types, mask=None): """Get packages that match a certain type. @@ -321,7 +322,7 @@ return presets[0] - def get_price_id_list(self, package_keyname, item_keynames): + def get_price_id_list(self, package_keyname, item_keynames, core=None): """Converts a list of item keynames to a list of price IDs. This function is used to convert a list of item keynames into @@ -330,14 +331,16 @@ :param str package_keyname: The package associated with the prices :param list item_keynames: A list of item keyname strings + :param str core: preset guest core capacity. :returns: A list of price IDs associated with the given item keynames in the given package """ - mask = 'id, keyName, prices' + mask = 'id, itemCategory, keyName, prices[categories]' items = self.list_items(package_keyname, mask=mask) prices = [] + gpu_number = -1 for item_keyname in item_keynames: try: # Need to find the item in the package that has a matching @@ -353,12 +356,64 @@ # because that is the most generic price. verifyOrder/placeOrder # can take that ID and create the proper price for us in the location # in which the order is made - price_id = [p['id'] for p in matching_item['prices'] - if not p['locationGroupId']][0] + if matching_item['itemCategory']['categoryCode'] != "gpu0": + price_id = self.get_item_price_id(core, matching_item['prices']) + else: + # GPU items has two generic prices and they are added to the list + # according to the number of gpu items added in the order. + gpu_number += 1 + price_id = [p['id'] for p in matching_item['prices'] + if not p['locationGroupId'] + and p['categories'][0]['categoryCode'] == "gpu" + str(gpu_number)][0] + prices.append(price_id) return prices + @staticmethod + def get_item_price_id(core, prices): + """get item price id""" + price_id = None + for price in prices: + if not price['locationGroupId']: + capacity_min = int(price.get('capacityRestrictionMinimum', -1)) + capacity_max = int(price.get('capacityRestrictionMaximum', -1)) + # return first match if no restirction, or no core to check + if capacity_min == -1 or core is None: + price_id = price['id'] + # this check is mostly to work nicely with preset configs + elif capacity_min <= int(core) <= capacity_max: + price_id = price['id'] + return price_id + + def get_preset_prices(self, preset): + """Get preset item prices. + + Retrieve a SoftLayer_Product_Package_Preset record. + + :param int preset: preset identifier. + :returns: A list of price IDs associated with the given preset_id. + + """ + mask = 'mask[prices[item]]' + + prices = self.package_preset.getObject(id=preset, mask=mask) + return prices + + def get_item_prices(self, package_id): + """Get item prices. + + Retrieve a SoftLayer_Product_Package item prices record. + + :param int package_id: package identifier. + :returns: A list of price IDs associated with the given package. + + """ + mask = 'mask[pricingLocationGroup[locations]]' + + prices = self.package_svc.getItemPrices(id=package_id, mask=mask) + return prices + def verify_order(self, package_keyname, location, item_keynames, complex_type=None, hourly=True, preset_keyname=None, extras=None, quantity=1): """Verifies an order with the given package and prices. @@ -420,6 +475,39 @@ extras=extras, quantity=quantity) return self.order_svc.placeOrder(order) + def place_quote(self, package_keyname, location, item_keynames, complex_type=None, + preset_keyname=None, extras=None, quantity=1, quote_name=None, send_email=False): + """Place a quote with the given package and prices. + + This function takes in parameters needed for an order and places the quote. + + :param str package_keyname: The keyname for the package being ordered + :param str location: The datacenter location string for ordering (Ex: DALLAS13) + :param list item_keynames: The list of item keyname strings to order. To see list of + possible keynames for a package, use list_items() + (or `slcli order item-list`) + :param str complex_type: The complex type to send with the order. Typically begins + with `SoftLayer_Container_Product_Order_`. + :param string preset_keyname: If needed, specifies a preset to use for that package. + To see a list of possible keynames for a package, use + list_preset() (or `slcli order preset-list`) + :param dict extras: The extra data for the order in dictionary format. + Example: A VSI order requires hostname and domain to be set, so + extras will look like the following: + {'virtualGuests': [{'hostname': 'test', domain': 'softlayer.com'}]} + :param int quantity: The number of resources to order + :param string quote_name: A custom name to be assigned to the quote (optional). + :param bool send_email: This flag indicates that the quote should be sent to the email + address associated with the account or order. + """ + order = self.generate_order(package_keyname, location, item_keynames, complex_type=complex_type, + hourly=False, preset_keyname=preset_keyname, extras=extras, quantity=quantity) + + order['quoteName'] = quote_name + order['sendQuoteEmailFlag'] = send_email + + return self.order_svc.placeQuote(order) + def generate_order(self, package_keyname, location, item_keynames, complex_type=None, hourly=True, preset_keyname=None, extras=None, quantity=1): """Generates an order with the given package and prices. @@ -445,6 +533,7 @@ :param int quantity: The number of resources to order """ + container = {} order = {} extras = extras or {} @@ -460,17 +549,25 @@ order['quantity'] = quantity order['useHourlyPricing'] = hourly + preset_core = None if preset_keyname: preset_id = self.get_preset_by_key(package_keyname, preset_keyname)['id'] + preset_items = self.get_preset_prices(preset_id) + for item in preset_items['prices']: + if item['item']['itemCategory']['categoryCode'] == "guest_core": + preset_core = item['item']['capacity'] order['presetId'] = preset_id if not complex_type: raise exceptions.SoftLayerError("A complex type must be specified with the order") order['complexType'] = complex_type - price_ids = self.get_price_id_list(package_keyname, item_keynames) + price_ids = self.get_price_id_list(package_keyname, item_keynames, preset_core) order['prices'] = [{'id': price_id} for price_id in price_ids] - return order + + container['orderContainers'] = [order] + + return container def package_locations(self, package_keyname): """List datacenter locations for a package keyname diff -Nru python-softlayer-5.4.4/SoftLayer/managers/ticket.py python-softlayer-5.6.4/SoftLayer/managers/ticket.py --- python-softlayer-5.4.4/SoftLayer/managers/ticket.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/ticket.py 2018-11-16 23:06:56.000000000 +0000 @@ -5,7 +5,6 @@ :license: MIT, see LICENSE for more details. """ - from SoftLayer import utils @@ -29,8 +28,8 @@ :param boolean open_status: include open tickets :param boolean closed_status: include closed tickets """ - mask = ('id, title, assignedUser[firstName, lastName],' - 'createDate,lastEditDate,accountId,status') + mask = """mask[id, title, assignedUser[firstName, lastName], priority, + createDate, lastEditDate, accountId, status, updateCount]""" call = 'getTickets' if not all([open_status, closed_status]): @@ -38,8 +37,10 @@ call = 'getOpenTickets' elif closed_status: call = 'getClosedTickets' + else: + raise ValueError("open_status and closed_status cannot both be False") - return self.client.call('Account', call, mask=mask) + return self.client.call('Account', call, mask=mask, iter=True) def list_subjects(self): """List all ticket subjects.""" @@ -52,18 +53,18 @@ :returns: dict -- information about the specified ticket """ - mask = ('id, title, assignedUser[firstName, lastName],status,' - 'createDate,lastEditDate,updates[entry,editor],updateCount') + mask = """mask[id, title, assignedUser[firstName, lastName],status, + createDate,lastEditDate,updates[entry,editor],updateCount, priority]""" return self.ticket.getObject(id=ticket_id, mask=mask) - def create_ticket(self, title=None, body=None, subject=None): + def create_ticket(self, title=None, body=None, subject=None, priority=None): """Create a new ticket. :param string title: title for the new ticket :param string body: body for the new ticket :param integer subject: id of the subject to be assigned to the ticket + :param integer priority: Value from 1 (highest) to 4 (lowest) """ - current_user = self.account.getCurrentUser() new_ticket = { 'subjectId': subject, @@ -71,6 +72,9 @@ 'assignedUserId': current_user['id'], 'title': title, } + if priority is not None: + new_ticket['priority'] = int(priority) + created_ticket = self.ticket.createStandardTicket(new_ticket, body) return created_ticket @@ -82,18 +86,12 @@ """ return self.ticket.addUpdate({'entry': body}, id=ticket_id) - def upload_attachment(self, ticket_id=None, file_path=None, - file_name=None): + def upload_attachment(self, ticket_id=None, file_path=None, file_name=None): """Upload an attachment to a ticket. - :param integer ticket_id: the id of the ticket to - upload the attachment to - :param string file_path: - The path of the attachment to be uploaded - :param string file_name: - The name of the attachment shown - in the ticket - + :param integer ticket_id: the id of the ticket to upload the attachment to + :param string file_path: The path of the attachment to be uploaded + :param string file_name: The name of the attachment shown in the ticket :returns: dict -- The uploaded attachment """ file_content = None diff -Nru python-softlayer-5.4.4/SoftLayer/managers/user.py python-softlayer-5.6.4/SoftLayer/managers/user.py --- python-softlayer-5.4.4/SoftLayer/managers/user.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/user.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,268 @@ +""" + SoftLayer.user + ~~~~~~~~~~~~~ + User Manager/helpers + + :license: MIT, see LICENSE for more details. +""" +import datetime +import logging +from operator import itemgetter + +from SoftLayer import exceptions +from SoftLayer import utils + +LOGGER = logging.getLogger(__name__) + + +class UserManager(utils.IdentifierMixin, object): + """Manages Users. + + See: https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/ + + Example:: + + # Initialize the Manager. + import SoftLayer + client = SoftLayer.create_client_from_env() + mgr = SoftLayer.UserManager(client) + + :param SoftLayer.API.BaseClient client: the client instance + + """ + + def __init__(self, client): + self.client = client + self.user_service = self.client['SoftLayer_User_Customer'] + self.account_service = self.client['SoftLayer_Account'] + self.resolvers = [self._get_id_from_username] + self.all_permissions = None + + def list_users(self, objectmask=None, objectfilter=None): + """Lists all users on an account + + :param string objectmask: Used to overwrite the default objectmask. + :param dictionary objectfilter: If you want to use an objectfilter. + :returns: A list of dictionaries that describe each user + + Example:: + result = mgr.list_users() + """ + + if objectmask is None: + objectmask = """mask[id, username, displayName, userStatus[name], hardwareCount, virtualGuestCount, + email, roles]""" + + return self.account_service.getUsers(mask=objectmask, filter=objectfilter) + + def get_user(self, user_id, objectmask=None): + """Calls SoftLayer_User_Customer::getObject + + :param int user_id: Id of the user + :param string objectmask: default is 'mask[userStatus[name], parent[id, username]]' + :returns: A user object. + """ + if objectmask is None: + objectmask = "mask[userStatus[name], parent[id, username]]" + return self.user_service.getObject(id=user_id, mask=objectmask) + + def get_current_user(self, objectmask=None): + """Calls SoftLayer_Account::getCurrentUser""" + + if objectmask is None: + objectmask = "mask[userStatus[name], parent[id, username]]" + return self.account_service.getCurrentUser(mask=objectmask) + + def get_all_permissions(self): + """Calls SoftLayer_User_CustomerPermissions_Permission::getAllObjects + + Stores the result in self.all_permissions + :returns: A list of dictionaries that contains all valid permissions + """ + if self.all_permissions is None: + permissions = self.client.call('User_Customer_CustomerPermission_Permission', 'getAllObjects') + self.all_permissions = sorted(permissions, key=itemgetter('keyName')) + return self.all_permissions + + def add_permissions(self, user_id, permissions): + """Enables a list of permissions for a user + + :param int id: user id to set + :param list permissions: List of permissions keynames to enable + :returns: True on success, Exception otherwise + + Example:: + add_permissions(123, ['BANDWIDTH_MANAGE']) + """ + pretty_permissions = self.format_permission_object(permissions) + LOGGER.warning("Adding the following permissions to %s: %s", user_id, pretty_permissions) + return self.user_service.addBulkPortalPermission(pretty_permissions, id=user_id) + + def remove_permissions(self, user_id, permissions): + """Disables a list of permissions for a user + + :param int id: user id to set + :param list permissions: List of permissions keynames to disable + :returns: True on success, Exception otherwise + + Example:: + remove_permissions(123, ['BANDWIDTH_MANAGE']) + """ + pretty_permissions = self.format_permission_object(permissions) + LOGGER.warning("Removing the following permissions to %s: %s", user_id, pretty_permissions) + return self.user_service.removeBulkPortalPermission(pretty_permissions, id=user_id) + + def permissions_from_user(self, user_id, from_user_id): + """Sets user_id's permission to be the same as from_user_id's + + Any permissions from_user_id has will be added to user_id. + Any permissions from_user_id doesn't have will be removed from user_id. + + :param int user_id: The user to change permissions. + :param int from_user_id: The use to base permissions from. + :returns: True on success, Exception otherwise. + """ + + from_permissions = self.get_user_permissions(from_user_id) + self.add_permissions(user_id, from_permissions) + all_permissions = self.get_all_permissions() + remove_permissions = [] + + for permission in all_permissions: + # If permission does not exist for from_user_id add it to the list to be removed + if _keyname_search(from_permissions, permission['keyName']): + continue + else: + remove_permissions.append({'keyName': permission['keyName']}) + + self.remove_permissions(user_id, remove_permissions) + return True + + def get_user_permissions(self, user_id): + """Returns a sorted list of a users permissions""" + permissions = self.user_service.getPermissions(id=user_id) + return sorted(permissions, key=itemgetter('keyName')) + + def get_logins(self, user_id, start_date=None): + """Gets the login history for a user, default start_date is 30 days ago + + :param int id: User id to get + :param string start_date: "%m/%d/%Y %H:%M:%s" formatted string. + :returns: list https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer_Access_Authentication/ + Example:: + get_logins(123, '04/08/2018 0:0:0') + """ + + if start_date is None: + date_object = datetime.datetime.today() - datetime.timedelta(days=30) + start_date = date_object.strftime("%m/%d/%Y 0:0:0") + + date_filter = { + 'loginAttempts': { + 'createDate': { + 'operation': 'greaterThanDate', + 'options': [{'name': 'date', 'value': [start_date]}] + } + } + } + login_log = self.user_service.getLoginAttempts(id=user_id, filter=date_filter) + return login_log + + def get_events(self, user_id, start_date=None): + """Gets the event log for a specific user, default start_date is 30 days ago + + :param int id: User id to view + :param string start_date: "%Y-%m-%dT%H:%M:%s.0000-06:00" is the full formatted string. + The Timezone part has to be HH:MM, notice the : there. + :returns: https://softlayer.github.io/reference/datatypes/SoftLayer_Event_Log/ + """ + + if start_date is None: + date_object = datetime.datetime.today() - datetime.timedelta(days=30) + start_date = date_object.strftime("%Y-%m-%dT00:00:00") + + object_filter = { + 'userId': { + 'operation': user_id + }, + 'eventCreateDate': { + 'operation': 'greaterThanDate', + 'options': [{'name': 'date', 'value': [start_date]}] + } + } + + events = self.client.call('Event_Log', 'getAllObjects', filter=object_filter) + if events is None: + events = [{'eventName': 'No Events Found'}] + return events + + def _get_id_from_username(self, username): + """Looks up a username's id + + :param string username: Username to lookup + :returns: The id that matches username. + """ + _mask = "mask[id, username]" + _filter = {'users': {'username': utils.query_filter(username)}} + user = self.list_users(_mask, _filter) + if len(user) == 1: + return [user[0]['id']] + elif len(user) > 1: + raise exceptions.SoftLayerError("Multiple users found with the name: %s" % username) + else: + raise exceptions.SoftLayerError("Unable to find user id for %s" % username) + + def format_permission_object(self, permissions): + """Formats a list of permission key names into something the SLAPI will respect. + + :param list permissions: A list of SLAPI permissions keyNames. + keyName of ALL will return all permissions. + :returns: list of dictionaries that can be sent to the api to add or remove permissions + :throws SoftLayerError: If any permission is invalid this exception will be thrown. + """ + pretty_permissions = [] + available_permissions = self.get_all_permissions() + # pp(available_permissions) + for permission in permissions: + # Handle data retrieved directly from the API + if isinstance(permission, dict): + permission = permission['keyName'] + permission = permission.upper() + if permission == 'ALL': + return available_permissions + # Search through available_permissions to make sure what the user entered was valid + if _keyname_search(available_permissions, permission): + pretty_permissions.append({'keyName': permission}) + else: + raise exceptions.SoftLayerError("'%s' is not a valid permission" % permission) + return pretty_permissions + + def create_user(self, user_object, password): + """Blindly sends user_object to SoftLayer_User_Customer::createObject + + :param dictionary user_object: https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/ + """ + LOGGER.warning("Creating User %s", user_object['username']) + return self.user_service.createObject(user_object, password, None) + + def edit_user(self, user_id, user_object): + """Blindly sends user_object to SoftLayer_User_Customer::editObject + + :param int user_id: User to edit + :param dictionary user_object: https://softlayer.github.io/reference/datatypes/SoftLayer_User_Customer/ + """ + return self.user_service.editObject(user_object, id=user_id) + + def add_api_authentication_key(self, user_id): + """Calls SoftLayer_User_Customer::addApiAuthenticationKey + + :param int user_id: User to add API key to + """ + return self.user_service.addApiAuthenticationKey(id=user_id) + + +def _keyname_search(haystack, needle): + for item in haystack: + if item.get('keyName') == needle: + return True + return False diff -Nru python-softlayer-5.4.4/SoftLayer/managers/vs_capacity.py python-softlayer-5.6.4/SoftLayer/managers/vs_capacity.py --- python-softlayer-5.4.4/SoftLayer/managers/vs_capacity.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/vs_capacity.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,174 @@ +""" + SoftLayer.vs_capacity + ~~~~~~~~~~~~~~~~~~~~~~~ + Reserved Capacity Manager and helpers + + :license: MIT, see License for more details. +""" + +import logging +import SoftLayer + +from SoftLayer.managers import ordering +from SoftLayer.managers.vs import VSManager +from SoftLayer import utils + +# Invalid names are ignored due to long method names and short argument names +# pylint: disable=invalid-name, no-self-use + +LOGGER = logging.getLogger(__name__) + + +class CapacityManager(utils.IdentifierMixin, object): + """Manages SoftLayer Reserved Capacity Groups. + + Product Information + + - https://console.bluemix.net/docs/vsi/vsi_about_reserved.html + - https://softlayer.github.io/reference/services/SoftLayer_Virtual_ReservedCapacityGroup/ + - https://softlayer.github.io/reference/services/SoftLayer_Virtual_ReservedCapacityGroup_Instance/ + + + :param SoftLayer.API.BaseClient client: the client instance + :param SoftLayer.managers.OrderingManager ordering_manager: an optional manager to handle ordering. + If none is provided, one will be auto initialized. + """ + + def __init__(self, client, ordering_manager=None): + self.client = client + self.account = client['Account'] + self.capacity_package = 'RESERVED_CAPACITY' + self.rcg_service = 'Virtual_ReservedCapacityGroup' + + if ordering_manager is None: + self.ordering_manager = ordering.OrderingManager(client) + + def list(self): + """List Reserved Capacities""" + mask = """mask[availableInstanceCount, occupiedInstanceCount, +instances[id, billingItem[description, hourlyRecurringFee]], instanceCount, backendRouter[datacenter]]""" + results = self.client.call('Account', 'getReservedCapacityGroups', mask=mask) + return results + + def get_object(self, identifier, mask=None): + """Get a Reserved Capacity Group + + :param int identifier: Id of the SoftLayer_Virtual_ReservedCapacityGroup + :param string mask: override default object Mask + """ + if mask is None: + mask = "mask[instances[billingItem[item[keyName],category], guest], backendRouter[datacenter]]" + result = self.client.call(self.rcg_service, 'getObject', id=identifier, mask=mask) + return result + + def get_create_options(self): + """List available reserved capacity plans""" + mask = "mask[attributes,prices[pricingLocationGroup]]" + results = self.ordering_manager.list_items(self.capacity_package, mask=mask) + return results + + def get_available_routers(self, dc=None): + """Pulls down all backendRouterIds that are available + + :param string dc: A specific location to get routers for, like 'dal13'. + :returns list: A list of locations where RESERVED_CAPACITY can be ordered. + """ + mask = "mask[locations]" + # Step 1, get the package id + package = self.ordering_manager.get_package_by_key(self.capacity_package, mask="id") + + # Step 2, get the regions this package is orderable in + regions = self.client.call('Product_Package', 'getRegions', id=package['id'], mask=mask, iter=True) + _filter = None + routers = {} + if dc is not None: + _filter = {'datacenterName': {'operation': dc}} + + # Step 3, for each location in each region, get the pod details, which contains the router id + pods = self.client.call('Network_Pod', 'getAllObjects', filter=_filter, iter=True) + for region in regions: + routers[region['keyname']] = [] + for location in region['locations']: + location['location']['pods'] = list() + for pod in pods: + if pod['datacenterName'] == location['location']['name']: + location['location']['pods'].append(pod) + + # Step 4, return the data. + return regions + + def create(self, name, backend_router_id, flavor, instances, test=False): + """Orders a Virtual_ReservedCapacityGroup + + :param string name: Name for the new reserved capacity + :param int backend_router_id: This selects the pod. See create_options for a list + :param string flavor: Capacity KeyName, see create_options for a list + :param int instances: Number of guest this capacity can support + :param bool test: If True, don't actually order, just test. + """ + + # Since orderManger needs a DC id, just send in 0, the API will ignore it + args = (self.capacity_package, 0, [flavor]) + extras = {"backendRouterId": backend_router_id, "name": name} + kwargs = { + 'extras': extras, + 'quantity': instances, + 'complex_type': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', + 'hourly': True + } + if test: + receipt = self.ordering_manager.verify_order(*args, **kwargs) + else: + receipt = self.ordering_manager.place_order(*args, **kwargs) + return receipt + + def create_guest(self, capacity_id, test, guest_object): + """Turns an empty Reserve Capacity into a real Virtual Guest + + :param int capacity_id: ID of the RESERVED_CAPACITY_GROUP to create this guest into + :param bool test: True will use verifyOrder, False will use placeOrder + :param dictionary guest_object: Below is the minimum info you need to send in + guest_object = { + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'os_code': 'UBUNTU_LATEST_64', + 'primary_disk': '25', + } + + """ + + vs_manager = VSManager(self.client) + mask = "mask[instances[id, billingItem[id, item[id,keyName]]], backendRouter[id, datacenter[name]]]" + capacity = self.get_object(capacity_id, mask=mask) + try: + capacity_flavor = capacity['instances'][0]['billingItem']['item']['keyName'] + flavor = _flavor_string(capacity_flavor, guest_object['primary_disk']) + except KeyError: + raise SoftLayer.SoftLayerError("Unable to find capacity Flavor.") + + guest_object['flavor'] = flavor + guest_object['datacenter'] = capacity['backendRouter']['datacenter']['name'] + + # Reserved capacity only supports SAN as of 20181008 + guest_object['local_disk'] = False + template = vs_manager.verify_create_instance(**guest_object) + template['reservedCapacityId'] = capacity_id + if guest_object.get('ipv6'): + ipv6_price = self.ordering_manager.get_price_id_list('PUBLIC_CLOUD_SERVER', ['1_IPV6_ADDRESS']) + template['prices'].append({'id': ipv6_price[0]}) + + if test: + result = self.client.call('Product_Order', 'verifyOrder', template) + else: + result = self.client.call('Product_Order', 'placeOrder', template) + + return result + + +def _flavor_string(capacity_key, primary_disk): + """Removed the _X_YEAR_TERM from capacity_key and adds the primary disk size, creating the flavor keyName + + This will work fine unless 10 year terms are invented... or flavor format changes... + """ + flavor = "%sX%s" % (capacity_key[:-12], primary_disk) + return flavor diff -Nru python-softlayer-5.4.4/SoftLayer/managers/vs.py python-softlayer-5.6.4/SoftLayer/managers/vs.py --- python-softlayer-5.4.4/SoftLayer/managers/vs.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/managers/vs.py 2018-11-16 23:06:56.000000000 +0000 @@ -16,8 +16,9 @@ from SoftLayer.managers import ordering from SoftLayer import utils - LOGGER = logging.getLogger(__name__) + + # pylint: disable=no-self-use @@ -50,6 +51,7 @@ self.client = client self.account = client['Account'] self.guest = client['Virtual_Guest'] + self.package_svc = client['Product_Package'] self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] if ordering_manager is None: self.ordering_manager = ordering.OrderingManager(client) @@ -157,8 +159,8 @@ utils.query_filter(private_ip)) kwargs['filter'] = _filter.to_dict() - func = getattr(self.account, call) - return func(**kwargs) + kwargs['iter'] = True + return self.client.call('Account', call, **kwargs) @retry(logger=LOGGER) def get_instance(self, instance_id, **kwargs): @@ -198,7 +200,7 @@ 'primaryIpAddress,' '''networkComponents[id, status, speed, maxSpeed, name, macAddress, primaryIpAddress, port, - primarySubnet, + primarySubnet[addressSpace], securityGroupBindings[ securityGroup[id, name]]],''' 'lastKnownPowerState.name,' @@ -208,6 +210,7 @@ 'maxMemory,' 'datacenter,' 'activeTransaction[id, transactionStatus[friendlyName,name]],' + 'lastTransaction[transactionStatus],' 'lastOperatingSystemReload.id,' 'blockDevices,' 'blockDeviceTemplateGroup[id, name, globalIdentifier],' @@ -224,6 +227,7 @@ 'hourlyBillingFlag,' 'userData,' '''billingItem[id,nextInvoiceTotalRecurringAmount, + package[id,keyName], children[categoryCode,nextInvoiceTotalRecurringAmount], orderItem[id, order.userRecord[username], @@ -305,6 +309,7 @@ hostname=None, domain=None, local_disk=True, datacenter=None, os_code=None, image_id=None, dedicated=False, public_vlan=None, private_vlan=None, + private_subnet=None, public_subnet=None, userdata=None, nic_speed=None, disks=None, post_uri=None, private=False, ssh_keys=None, public_security_groups=None, private_security_groups=None, boot_mode=None, **kwargs): @@ -365,14 +370,10 @@ if datacenter: data["datacenter"] = {"name": datacenter} - if public_vlan: - data.update({ - 'primaryNetworkComponent': { - "networkVlan": {"id": int(public_vlan)}}}) - if private_vlan: - data.update({ - "primaryBackendNetworkComponent": { - "networkVlan": {"id": int(private_vlan)}}}) + if private_vlan or public_vlan or private_subnet or public_subnet: + network_components = self._create_network_components(public_vlan, private_vlan, + private_subnet, public_subnet) + data.update(network_components) if public_security_groups: secgroups = [{'securityGroup': {'id': int(sg)}} @@ -415,6 +416,29 @@ return data + def _create_network_components( + self, public_vlan=None, private_vlan=None, + private_subnet=None, public_subnet=None): + + parameters = {} + if private_vlan: + parameters['primaryBackendNetworkComponent'] = {"networkVlan": {"id": int(private_vlan)}} + if public_vlan: + parameters['primaryNetworkComponent'] = {"networkVlan": {"id": int(public_vlan)}} + if public_subnet: + if public_vlan is None: + raise exceptions.SoftLayerError("You need to specify a public_vlan with public_subnet") + else: + parameters['primaryNetworkComponent']['networkVlan']['primarySubnet'] = {'id': int(public_subnet)} + if private_subnet: + if private_vlan is None: + raise exceptions.SoftLayerError("You need to specify a private_vlan with private_subnet") + else: + parameters['primaryBackendNetworkComponent']['networkVlan']['primarySubnet'] = { + "id": int(private_subnet)} + + return parameters + @retry(logger=LOGGER) def wait_for_transaction(self, instance_id, limit, delay=10): """Waits on a VS transaction for the specified amount of time. @@ -479,15 +503,16 @@ 'domain': u'test01.labs.sftlyr.ws', 'hostname': u'minion05', 'datacenter': u'hkg02', + 'flavor': 'BL1_1X2X100' 'dedicated': False, 'private': False, - 'cpus': 1, 'os_code' : u'UBUNTU_LATEST', 'hourly': True, 'ssh_keys': [1234], 'disks': ('100','25'), 'local_disk': True, - 'memory': 1024 + 'tags': 'test, pleaseCancel', + 'public_security_groups': [12, 15] } vsi = mgr.verify_create_instance(**new_vsi) @@ -512,15 +537,14 @@ 'domain': u'test01.labs.sftlyr.ws', 'hostname': u'minion05', 'datacenter': u'hkg02', + 'flavor': 'BL1_1X2X100' 'dedicated': False, 'private': False, - 'cpus': 1, 'os_code' : u'UBUNTU_LATEST', 'hourly': True, 'ssh_keys': [1234], 'disks': ('100','25'), 'local_disk': True, - 'memory': 1024, 'tags': 'test, pleaseCancel', 'public_security_groups': [12, 15] } @@ -583,17 +607,16 @@ # Define the instance we want to create. new_vsi = { 'domain': u'test01.labs.sftlyr.ws', - 'hostname': u'multi-test', + 'hostname': u'minion05', 'datacenter': u'hkg02', + 'flavor': 'BL1_1X2X100' 'dedicated': False, 'private': False, - 'cpus': 1, 'os_code' : u'UBUNTU_LATEST', 'hourly': True, - 'ssh_keys': [87634], + 'ssh_keys': [1234], 'disks': ('100','25'), 'local_disk': True, - 'memory': 1024, 'tags': 'test, pleaseCancel', 'public_security_groups': [12, 15] } @@ -782,7 +805,7 @@ name, disks_to_capture, notes, id=instance_id) def upgrade(self, instance_id, cpus=None, memory=None, - nic_speed=None, public=True): + nic_speed=None, public=True, preset=None): """Upgrades a VS instance. Example:: @@ -796,6 +819,7 @@ :param int instance_id: Instance id of the VS to be upgraded :param int cpus: The number of virtual CPUs to upgrade to of a VS instance. + :param string preset: preset assigned to the vsi :param int memory: RAM of the VS to be upgraded to. :param int nic_speed: The port speed to set :param bool public: CPU will be in Private/Public Node. @@ -805,9 +829,28 @@ upgrade_prices = self._get_upgrade_prices(instance_id) prices = [] - for option, value in {'cpus': cpus, - 'memory': memory, - 'nic_speed': nic_speed}.items(): + data = {'nic_speed': nic_speed} + + if cpus is not None and preset is not None: + raise exceptions.SoftLayerError("Do not use cpu, private and memory if you are using flavors") + data['cpus'] = cpus + + if memory is not None and preset is not None: + raise exceptions.SoftLayerError("Do not use memory, private or cpu if you are using flavors") + data['memory'] = memory + + maintenance_window = datetime.datetime.now(utils.UTC()) + order = { + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_Guest_' + 'Upgrade', + 'properties': [{ + 'name': 'MAINTENANCE_WINDOW', + 'value': maintenance_window.strftime("%Y-%m-%d %H:%M:%S%z") + }], + 'virtualGuests': [{'id': int(instance_id)}], + } + + for option, value in data.items(): if not value: continue price_id = self._get_price_id_for_upgrade_option(upgrade_prices, @@ -820,19 +863,13 @@ "Unable to find %s option with value %s" % (option, value)) prices.append({'id': price_id}) + order['prices'] = prices - maintenance_window = datetime.datetime.now(utils.UTC()) - order = { - 'complexType': 'SoftLayer_Container_Product_Order_Virtual_Guest_' - 'Upgrade', - 'prices': prices, - 'properties': [{ - 'name': 'MAINTENANCE_WINDOW', - 'value': maintenance_window.strftime("%Y-%m-%d %H:%M:%S%z") - }], - 'virtualGuests': [{'id': int(instance_id)}], - } - if prices: + if preset is not None: + vs_object = self.get_instance(instance_id)['billingItem']['package'] + order['presetId'] = self.ordering_manager.get_preset_by_key(vs_object['keyName'], preset)['id'] + + if prices or preset: self.client['Product_Order'].placeOrder(order) return True return False diff -Nru python-softlayer-5.4.4/SoftLayer/shell/completer.py python-softlayer-5.6.4/SoftLayer/shell/completer.py --- python-softlayer-5.4.4/SoftLayer/shell/completer.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/shell/completer.py 2018-11-16 23:06:56.000000000 +0000 @@ -14,6 +14,7 @@ class ShellCompleter(completion.Completer): """Completer for the shell.""" + def __init__(self, click_root): self.root = click_root diff -Nru python-softlayer-5.4.4/SoftLayer/shell/core.py python-softlayer-5.6.4/SoftLayer/shell/core.py --- python-softlayer-5.4.4/SoftLayer/shell/core.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/shell/core.py 2018-11-16 23:06:56.000000000 +0000 @@ -14,9 +14,7 @@ import click from prompt_toolkit import auto_suggest as p_auto_suggest -from prompt_toolkit import history as p_history from prompt_toolkit import shortcuts as p_shortcuts -from pygments import token from SoftLayer.CLI import core from SoftLayer.CLI import environment @@ -49,33 +47,14 @@ app_path = click.get_app_dir('softlayer_shell') if not os.path.exists(app_path): os.makedirs(app_path) - history = p_history.FileHistory(os.path.join(app_path, 'history')) complete = completer.ShellCompleter(core.cli) while True: - def get_prompt_tokens(_): - """Returns tokens for the command prompt""" - tokens = [] - try: - tokens.append((token.Token.Username, env.client.auth.username)) - tokens.append((token.Token.At, "@")) - except AttributeError: - pass - - tokens.append((token.Token.Host, "slcli-shell")) - if env.vars['last_exit_code']: - tokens.append((token.Token.ErrorPrompt, '> ')) - else: - tokens.append((token.Token.Prompt, '> ')) - - return tokens - try: line = p_shortcuts.prompt( completer=complete, - history=history, + complete_while_typing=True, auto_suggest=p_auto_suggest.AutoSuggestFromHistory(), - get_prompt_tokens=get_prompt_tokens, ) # Parse arguments diff -Nru python-softlayer-5.4.4/SoftLayer/testing/__init__.py python-softlayer-5.6.4/SoftLayer/testing/__init__.py --- python-softlayer-5.4.4/SoftLayer/testing/__init__.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/testing/__init__.py 2018-11-16 23:06:56.000000000 +0000 @@ -19,7 +19,6 @@ from SoftLayer.CLI import environment from SoftLayer.testing import xmlrpc - FIXTURE_PATH = os.path.abspath(os.path.join(__file__, '..', '..', 'fixtures')) @@ -160,11 +159,7 @@ """Set and return mock on the current client.""" return self.mocks.set_mock(service, method) - def run_command(self, - args=None, - env=None, - fixtures=True, - fmt='json'): + def run_command(self, args=None, env=None, fixtures=True, fmt='json'): """A helper that runs a SoftLayer CLI command. This returns a click.testing.Result object. @@ -185,7 +180,7 @@ for prop, expected_value in props.items(): actual_value = getattr(call, prop) if actual_value != expected_value: - logging.info( + logging.critical( '%s::%s property mismatch, %s: expected=%r; actual=%r', call.service, call.method, diff -Nru python-softlayer-5.4.4/SoftLayer/transports.py python-softlayer-5.6.4/SoftLayer/transports.py --- python-softlayer-5.4.4/SoftLayer/transports.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/transports.py 2018-11-16 23:06:56.000000000 +0000 @@ -120,21 +120,31 @@ #: Exception any exceptions that got caught self.exception = None + def __repr__(self): + """Prints out what this call is all about""" + pretty_mask = utils.clean_string(self.mask) + pretty_filter = self.filter + param_string = "id={id}, mask='{mask}', filter='{filter}', args={args}, limit={limit}, offset={offset}".format( + id=self.identifier, mask=pretty_mask, filter=pretty_filter, + args=self.args, limit=self.limit, offset=self.offset) + return "{service}::{method}({params})".format( + service=self.service, method=self.method, params=param_string) + class SoftLayerListResult(list): """A SoftLayer API list result.""" - def __init__(self, items, total_count): + def __init__(self, items=None, total_count=0): #: total count of items that exist on the server. This is useful when #: paginating through a large list of objects. self.total_count = total_count - super(SoftLayerListResult, self).__init__(items) class XmlRpcTransport(object): """XML-RPC transport.""" + def __init__(self, endpoint_url=None, timeout=None, proxy=None, user_agent=None, verify=True): self.endpoint_url = (endpoint_url or consts.API_PUBLIC_ENDPOINT).rstrip('/') @@ -433,13 +443,14 @@ self.requests.append(call) if call.exception is not None: + LOGGER.debug(self.print_reproduceable(call)) raise call.exception return call.result def pre_transport_log(self, call): """Prints a warning before calling the API """ - output = "Calling: {}::{}(id={})".format(call.service, call.method, call.identifier) + output = "Calling: {})".format(call) LOGGER.warning(output) def post_transport_log(self, call): @@ -490,6 +501,7 @@ class FixtureTransport(object): """Implements a transport which returns fixtures.""" + def __call__(self, call): """Load fixture from the default fixture path.""" try: diff -Nru python-softlayer-5.4.4/SoftLayer/utils.py python-softlayer-5.6.4/SoftLayer/utils.py --- python-softlayer-5.4.4/SoftLayer/utils.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/SoftLayer/utils.py 2018-11-16 23:06:56.000000000 +0000 @@ -209,3 +209,18 @@ if instance.get('provisionDate') and not reloading and not outstanding: return True return False + + +def clean_string(string): + """Returns a string with all newline and other whitespace garbage removed. + + Mostly this method is used to print out objectMasks that have a lot of extra whitespace + in them because making compact masks in python means they don't look nice in the IDE. + + :param string: The string to clean. + :returns string: A string without extra whitespace. + """ + if string is None: + return '' + else: + return " ".join(string.split()) diff -Nru python-softlayer-5.4.4/tests/api_tests.py python-softlayer-5.6.4/tests/api_tests.py --- python-softlayer-5.4.4/tests/api_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/api_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -12,7 +12,7 @@ from SoftLayer import transports -class Inititialization(testing.TestCase): +class Initialization(testing.TestCase): def test_init(self): client = SoftLayer.Client(username='doesnotexist', api_key='issurelywrong', @@ -144,7 +144,10 @@ @mock.patch('SoftLayer.API.BaseClient.call') def test_iter_call(self, _call): # chunk=100, no limit - _call.side_effect = [list(range(100)), list(range(100, 125))] + _call.side_effect = [ + transports.SoftLayerListResult(range(100), 125), + transports.SoftLayerListResult(range(100, 125), 125) + ] result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True)) self.assertEqual(list(range(125)), result) @@ -155,7 +158,11 @@ _call.reset_mock() # chunk=100, no limit. Requires one extra request. - _call.side_effect = [list(range(100)), list(range(100, 200)), []] + _call.side_effect = [ + transports.SoftLayerListResult(range(100), 201), + transports.SoftLayerListResult(range(100, 200), 201), + transports.SoftLayerListResult([], 201) + ] result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True)) self.assertEqual(list(range(200)), result) _call.assert_has_calls([ @@ -166,13 +173,16 @@ _call.reset_mock() # chunk=25, limit=30 - _call.side_effect = [list(range(0, 25)), list(range(25, 30))] + _call.side_effect = [ + transports.SoftLayerListResult(range(0, 25), 30), + transports.SoftLayerListResult(range(25, 30), 30) + ] result = list(self.client.iter_call( - 'SERVICE', 'METHOD', iter=True, limit=30, chunk=25)) + 'SERVICE', 'METHOD', iter=True, limit=25)) self.assertEqual(list(range(30)), result) _call.assert_has_calls([ mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=0), - mock.call('SERVICE', 'METHOD', iter=False, limit=5, offset=25), + mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=25), ]) _call.reset_mock() @@ -185,26 +195,27 @@ ]) _call.reset_mock() - # chunk=25, limit=30, offset=12 - _call.side_effect = [list(range(0, 25)), list(range(25, 30))] + _call.side_effect = [ + transports.SoftLayerListResult(range(0, 25), 30), + transports.SoftLayerListResult(range(25, 30), 30) + ] result = list(self.client.iter_call('SERVICE', 'METHOD', 'ARG', iter=True, - limit=30, - chunk=25, + limit=25, offset=12)) self.assertEqual(list(range(30)), result) _call.assert_has_calls([ mock.call('SERVICE', 'METHOD', 'ARG', iter=False, limit=25, offset=12), mock.call('SERVICE', 'METHOD', 'ARG', - iter=False, limit=5, offset=37), + iter=False, limit=25, offset=37), ]) # Chunk size of 0 is invalid self.assertRaises( AttributeError, lambda: list(self.client.iter_call('SERVICE', 'METHOD', - iter=True, chunk=0))) + iter=True, limit=0))) def test_call_invalid_arguments(self): self.assertRaises( diff -Nru python-softlayer-5.4.4/tests/CLI/helper_tests.py python-softlayer-5.6.4/tests/CLI/helper_tests.py --- python-softlayer-5.4.4/tests/CLI/helper_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/helper_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -252,18 +252,15 @@ class ResolveIdTests(testing.TestCase): def test_resolve_id_one(self): - resolver = lambda r: [12345] - self.assertEqual(helpers.resolve_id(resolver, 'test'), 12345) + self.assertEqual(helpers.resolve_id(lambda r: [12345], 'test'), 12345) def test_resolve_id_none(self): - resolver = lambda r: [] self.assertRaises( - exceptions.CLIAbort, helpers.resolve_id, resolver, 'test') + exceptions.CLIAbort, helpers.resolve_id, lambda r: [], 'test') def test_resolve_id_multiple(self): - resolver = lambda r: [12345, 54321] self.assertRaises( - exceptions.CLIAbort, helpers.resolve_id, resolver, 'test') + exceptions.CLIAbort, helpers.resolve_id, lambda r: [12345, 54321], 'test') class TestTable(testing.TestCase): diff -Nru python-softlayer-5.4.4/tests/CLI/modules/block_tests.py python-softlayer-5.6.4/tests/CLI/modules/block_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/block_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/block_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -59,6 +59,7 @@ result = self.run_command(['block', 'volume-detail', '1234']) self.assert_no_fail(result) + isinstance(json.loads(result.output)['IOPs'], float) self.assertEqual({ 'Username': 'username', 'LUN Id': '2', @@ -107,6 +108,7 @@ 'capacity_gb': 20, 'datacenter': 'dal05', 'id': 100, + 'iops': None, 'ip_addr': '10.1.2.3', 'lunId': None, 'rep_partner_count': None, @@ -141,14 +143,6 @@ self.assertEqual(2, result.exit_code) - def test_volume_order_performance_iops_not_multiple_of_100(self): - result = self.run_command(['block', 'volume-order', - '--storage-type=performance', '--size=20', - '--iops=122', '--os-type=linux', - '--location=dal05']) - - self.assertEqual(2, result.exit_code) - def test_volume_order_performance_snapshot_error(self): result = self.run_command(['block', 'volume-order', '--storage-type=performance', '--size=20', @@ -202,7 +196,7 @@ {'description': '0.25 IOPS per GB'}, {'description': '20 GB Storage Space'}, {'description': '10 GB Storage Space (Snapshot Space)'}] - } + } } result = self.run_command(['block', 'volume-order', @@ -250,7 +244,7 @@ {'description': 'Block Storage'}, {'description': '20 GB Storage Space'}, {'description': '200 IOPS'}] - } + } } result = self.run_command(['block', 'volume-order', @@ -416,7 +410,7 @@ 'items': [{'description': '10 GB Storage Space (Snapshot Space)'}], 'status': 'PENDING_APPROVAL', - } + } } result = self.run_command(['block', 'snapshot-order', '1234', diff -Nru python-softlayer-5.4.4/tests/CLI/modules/dedicatedhost_tests.py python-softlayer-5.6.4/tests/CLI/modules/dedicatedhost_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/dedicatedhost_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/dedicatedhost_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -6,7 +6,6 @@ """ import json import mock -import os import SoftLayer from SoftLayer.CLI import exceptions @@ -29,21 +28,17 @@ 'datacenter': 'dal05', 'diskCapacity': 1200, 'guestCount': 1, - 'id': 44701, + 'id': 12345, 'memoryCapacity': 242, - 'name': 'khnguyendh' + 'name': 'test-dedicated' }] ) - def tear_down(self): - if os.path.exists("test.txt"): - os.remove("test.txt") - def test_details(self): mock = self.set_mock('SoftLayer_Virtual_DedicatedHost', 'getObject') mock.return_value = SoftLayer_Virtual_DedicatedHost.getObjectById - result = self.run_command(['dedicatedhost', 'detail', '44701', '--price', '--guests']) + result = self.run_command(['dedicatedhost', 'detail', '12345', '--price', '--guests']) self.assert_no_fail(result) self.assertEqual(json.loads(result.output), @@ -54,19 +49,19 @@ 'disk capacity': 1200, 'guest count': 1, 'guests': [{ - 'domain': 'Softlayer.com', - 'hostname': 'khnguyenDHI', - 'id': 43546081, - 'uuid': '806a56ec-0383-4c2e-e6a9-7dc89c4b29a2' + 'domain': 'test.com', + 'hostname': 'test-dedicated', + 'id': 12345, + 'uuid': 'F9329795-4220-4B0A-B970-C86B950667FA' }], - 'id': 44701, + 'id': 12345, 'memory capacity': 242, 'modify date': '2017-11-06T11:38:20-06:00', - 'name': 'khnguyendh', - 'owner': '232298_khuong', + 'name': 'test-dedicated', + 'owner': 'test-dedicated', 'price_rate': 1515.556, 'router hostname': 'bcr01a.dal05', - 'router id': 51218} + 'router id': 12345} ) def test_details_no_owner(self): @@ -85,18 +80,18 @@ 'disk capacity': 1200, 'guest count': 1, 'guests': [{ - 'domain': 'Softlayer.com', - 'hostname': 'khnguyenDHI', - 'id': 43546081, - 'uuid': '806a56ec-0383-4c2e-e6a9-7dc89c4b29a2'}], - 'id': 44701, + 'domain': 'test.com', + 'hostname': 'test-dedicated', + 'id': 12345, + 'uuid': 'F9329795-4220-4B0A-B970-C86B950667FA'}], + 'id': 12345, 'memory capacity': 242, 'modify date': '2017-11-06T11:38:20-06:00', - 'name': 'khnguyendh', + 'name': 'test-dedicated', 'owner': None, 'price_rate': 0, 'router hostname': 'bcr01a.dal05', - 'router id': 51218} + 'router id': 12345} ) def test_create_options(self): @@ -116,7 +111,7 @@ '56 Cores X 242 RAM X 1.2 TB', 'value': '56_CORES_X_242_RAM_X_1_4_TB' } - ]] + ]] ) def test_create_options_with_only_datacenter(self): @@ -137,18 +132,18 @@ self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [[ { - "Available Backend Routers": "bcr01a.dal05" + 'Available Backend Routers': 'bcr01a.dal05' }, { - "Available Backend Routers": "bcr02a.dal05" + 'Available Backend Routers': 'bcr02a.dal05' }, { - "Available Backend Routers": "bcr03a.dal05" + 'Available Backend Routers': 'bcr03a.dal05' }, { - "Available Backend Routers": "bcr04a.dal05" + 'Available Backend Routers': 'bcr04a.dal05' } - ]] + ]] ) def test_create(self): @@ -159,32 +154,66 @@ mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dedicatedhost', 'create', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=hourly']) self.assert_no_fail(result) args = ({ 'hardware': [{ - 'domain': 'example.com', + 'domain': 'test.com', 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, - 'hostname': 'host' + 'hostname': 'test-dedicated' + }], + 'useHourlyPricing': True, + 'location': 'DALLAS05', + 'packageId': 813, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'prices': [{ + 'id': 200269 + }], + 'quantity': 1},) + + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder', + args=args) + + def test_create_with_gpu(self): + SoftLayer.CLI.formatting.confirm = mock.Mock() + SoftLayer.CLI.formatting.confirm.return_value = True + mock_package_obj = self.set_mock('SoftLayer_Product_Package', + 'getAllObjects') + mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDHGpu + + result = self.run_command(['dedicatedhost', 'create', + '--hostname=test-dedicated', + '--domain=test.com', + '--datacenter=dal05', + '--flavor=56_CORES_X_484_RAM_X_1_5_TB_X_2_GPU_P100', + '--billing=hourly']) + self.assert_no_fail(result) + args = ({ + 'hardware': [{ + 'domain': 'test.com', + 'primaryBackendNetworkComponent': { + 'router': { + 'id': 12345 + } + }, + 'hostname': 'test-dedicated' }], 'prices': [{ 'id': 200269 }], 'location': 'DALLAS05', 'packageId': 813, - 'complexType': - 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'useHourlyPricing': True, - 'quantity': 1}, - ) + 'quantity': 1},) self.assert_called_with('SoftLayer_Product_Order', 'placeOrder', args=args) @@ -199,8 +228,8 @@ result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=hourly']) @@ -210,12 +239,12 @@ 'useHourlyPricing': True, 'hardware': [{ - 'hostname': 'host', - 'domain': 'example.com', + 'hostname': 'test-dedicated', + 'domain': 'test.com', 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } } }], @@ -229,8 +258,8 @@ result = self.run_command(['dh', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=monthly']) @@ -239,12 +268,12 @@ args = ({ 'useHourlyPricing': True, 'hardware': [{ - 'hostname': 'host', - 'domain': 'example.com', + 'hostname': 'test-dedicated', + 'domain': 'test.com', 'primaryBackendNetworkComponent': { - 'router': { - 'id': 51218 - } + 'router': { + 'id': 12345 + } } }], 'packageId': 813, 'prices': [{'id': 200269}], @@ -262,8 +291,8 @@ mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dh', 'create', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=monthly']) @@ -271,23 +300,6 @@ self.assertEqual(result.exit_code, 2) self.assertIsInstance(result.exception, exceptions.CLIAbort) - def test_create_export(self): - mock_package_obj = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH - mock_package = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') - mock_package.return_value = SoftLayer_Product_Package.verifyOrderDH - - self.run_command(['dedicatedhost', 'create', - '--verify', - '--hostname=host', - '--domain=example.com', - '--datacenter=dal05', - '--flavor=56_CORES_X_242_RAM_X_1_4_TB', - '--billing=hourly', - '--export=test.txt']) - - self.assertEqual(os.path.exists("test.txt"), True) - def test_create_verify_no_price_or_more_than_one(self): mock_package_obj = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH @@ -298,30 +310,97 @@ result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=hourly']) self.assertIsInstance(result.exception, exceptions.ArgumentError) args = ({ - 'hardware': [{ - 'domain': 'example.com', - 'primaryBackendNetworkComponent': { - 'router': { - 'id': 51218 - } - }, - 'hostname': 'host' - }], - 'prices': [{ - 'id': 200269 - }], - 'location': 'DALLAS05', - 'packageId': 813, - 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', - 'useHourlyPricing': True, - 'quantity': 1},) + 'hardware': [{ + 'domain': 'test.com', + 'primaryBackendNetworkComponent': { + 'router': { + 'id': 12345 + } + }, + 'hostname': 'test-dedicated' + }], + 'prices': [{ + 'id': 200269 + }], + 'location': 'DALLAS05', + 'packageId': 813, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'useHourlyPricing': True, + 'quantity': 1},) self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', args=args) + + @mock.patch('SoftLayer.DedicatedHostManager.cancel_host') + def test_cancel_host(self, cancel_mock): + result = self.run_command(['--really', 'dedicatedhost', 'cancel', '12345']) + + self.assert_no_fail(result) + cancel_mock.assert_called_with(12345) + + self.assertEqual(str(result.output), 'Dedicated Host 12345 was cancelled\n') + + def test_cancel_host_abort(self): + result = self.run_command(['dedicatedhost', 'cancel', '12345']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + def test_cancel_guests(self): + vs1 = {'id': 987, 'fullyQualifiedDomainName': 'foobar.example.com'} + vs2 = {'id': 654, 'fullyQualifiedDomainName': 'wombat.example.com'} + guests = self.set_mock('SoftLayer_Virtual_DedicatedHost', 'getGuests') + guests.return_value = [vs1, vs2] + + vs_status1 = {'id': 987, 'server name': 'foobar.example.com', 'status': 'Cancelled'} + vs_status2 = {'id': 654, 'server name': 'wombat.example.com', 'status': 'Cancelled'} + expected_result = [vs_status1, vs_status2] + + result = self.run_command(['--really', 'dedicatedhost', 'cancel-guests', '12345']) + self.assert_no_fail(result) + + self.assertEqual(expected_result, json.loads(result.output)) + + def test_cancel_guests_empty_list(self): + guests = self.set_mock('SoftLayer_Virtual_DedicatedHost', 'getGuests') + guests.return_value = [] + + result = self.run_command(['--really', 'dedicatedhost', 'cancel-guests', '12345']) + self.assert_no_fail(result) + + self.assertEqual(str(result.output), 'There is not any guest into the dedicated host 12345\n') + + def test_cancel_guests_abort(self): + result = self.run_command(['dedicatedhost', 'cancel-guests', '12345']) + self.assertEqual(result.exit_code, 2) + + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + def test_list_guests(self): + result = self.run_command(['dh', 'list-guests', '123', '--tag=tag']) + + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), + [{'hostname': 'vs-test1', + 'domain': 'test.sftlyr.ws', + 'primary_ip': '172.16.240.2', + 'id': 200, + 'power_state': 'Running', + 'backend_ip': '10.45.19.37'}, + {'hostname': 'vs-test2', + 'domain': 'test.sftlyr.ws', + 'primary_ip': '172.16.240.7', + 'id': 202, + 'power_state': 'Running', + 'backend_ip': '10.45.19.35'}]) + + def _get_cancel_guests_return(self): + vs_status1 = {'id': 123, 'fqdn': 'foobar.example.com', 'status': 'Cancelled'} + vs_status2 = {'id': 456, 'fqdn': 'wombat.example.com', 'status': 'Cancelled'} + return [vs_status1, vs_status2] diff -Nru python-softlayer-5.4.4/tests/CLI/modules/dns_tests.py python-softlayer-5.6.4/tests/CLI/modules/dns_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/dns_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/dns_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -72,11 +72,41 @@ 'ttl': 7200}) def test_add_record(self): - result = self.run_command(['dns', 'record-add', '1234', 'hostname', - 'A', 'd', '--ttl=100']) + result = self.run_command(['dns', 'record-add', 'hostname', 'A', + 'data', '--zone=1234', '--ttl=100']) self.assert_no_fail(result) - self.assertEqual(result.output, "") + self.assertEqual(str(result.output), 'A record added successfully\n') + + def test_add_record_mx(self): + result = self.run_command(['dns', 'record-add', 'hostname', 'MX', + 'data', '--zone=1234', '--ttl=100', '--priority=25']) + + self.assert_no_fail(result) + self.assertEqual(str(result.output), 'MX record added successfully\n') + + def test_add_record_srv(self): + result = self.run_command(['dns', 'record-add', 'hostname', 'SRV', + 'data', '--zone=1234', '--protocol=udp', + '--port=88', '--ttl=100', '--weight=5']) + + self.assert_no_fail(result) + self.assertEqual(str(result.output), 'SRV record added successfully\n') + + def test_add_record_ptr(self): + result = self.run_command(['dns', 'record-add', '192.168.1.1', 'PTR', + 'hostname', '--ttl=100']) + + self.assert_no_fail(result) + self.assertEqual(str(result.output), 'PTR record added successfully\n') + + def test_add_record_abort(self): + result = self.run_command(['dns', 'record-add', 'hostname', 'A', + 'data', '--ttl=100']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + self.assertEqual(result.exception.message, "A isn't a valid record type or zone is missing") @mock.patch('SoftLayer.CLI.formatting.no_going_back') def test_delete_record(self, no_going_back_mock): diff -Nru python-softlayer-5.4.4/tests/CLI/modules/file_tests.py python-softlayer-5.6.4/tests/CLI/modules/file_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/file_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/file_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -144,13 +144,6 @@ self.assertEqual(2, result.exit_code) - def test_volume_order_performance_iops_not_multiple_of_100(self): - result = self.run_command(['file', 'volume-order', - '--storage-type=performance', '--size=20', - '--iops=122', '--location=dal05']) - - self.assertEqual(2, result.exit_code) - def test_volume_order_performance_snapshot_error(self): result = self.run_command(['file', 'volume-order', '--storage-type=performance', '--size=20', @@ -204,7 +197,7 @@ {'description': '0.25 IOPS per GB'}, {'description': '20 GB Storage Space'}, {'description': '10 GB Storage Space (Snapshot Space)'}] - } + } } result = self.run_command(['file', 'volume-order', @@ -401,7 +394,7 @@ 'items': [{'description': '10 GB Storage Space (Snapshot Space)'}], 'status': 'PENDING_APPROVAL', - } + } } result = self.run_command(['file', 'snapshot-order', '1234', diff -Nru python-softlayer-5.4.4/tests/CLI/modules/nas_tests.py python-softlayer-5.6.4/tests/CLI/modules/nas_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/nas_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/nas_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -19,3 +19,32 @@ 'server': '127.0.0.1', 'id': 1, 'size': 10}]) + + def test_nas_credentials(self): + result = self.run_command(['nas', 'credentials', '12345']) + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), + [{ + 'password': '', + 'username': 'username' + }]) + + def test_server_credentials_exception_password_not_found(self): + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + + mock.return_value = { + "accountId": 11111, + "capacityGb": 20, + "id": 22222, + "nasType": "NAS", + "serviceProviderId": 1, + "username": "SL01SEV307", + "credentials": [] + } + + result = self.run_command(['nas', 'credentials', '12345']) + + self.assertEqual( + 'None', + str(result.exception) + ) diff -Nru python-softlayer-5.4.4/tests/CLI/modules/order_tests.py python-softlayer-5.6.4/tests/CLI/modules/order_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/order_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/order_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -47,26 +47,21 @@ self.assertEqual(expected_results, json.loads(result.output)) def test_package_list(self): - item1 = {'name': 'package1', 'keyName': 'PACKAGE1', 'type': {'keyName': 'BARE_METAL_CPU'}, 'isActive': 1} - item2 = {'name': 'package2', 'keyName': 'PACKAGE2', 'type': {'keyName': 'BARE_METAL_CPU'}, 'isActive': 1} - item3 = {'name': 'package2', 'keyName': 'PACKAGE2', 'type': {'keyName': 'BARE_METAL_CPU'}, 'isActive': 0} p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - p_mock.return_value = [item1, item2, item3] + p_mock.return_value = _get_all_packages() _filter = {'type': {'keyName': {'operation': '!= BLUEMIX_SERVICE'}}} result = self.run_command(['order', 'package-list']) self.assert_no_fail(result) self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects', filter=_filter) - expected_results = [{'name': 'package1', 'keyName': 'PACKAGE1', 'type': 'BARE_METAL_CPU'}, - {'name': 'package2', 'keyName': 'PACKAGE2', 'type': 'BARE_METAL_CPU'}] + expected_results = [{'id': 1, 'name': 'package1', 'keyName': 'PACKAGE1', 'type': 'BARE_METAL_CPU'}, + {'id': 2, 'name': 'package2', 'keyName': 'PACKAGE2', 'type': 'BARE_METAL_CPU'}] self.assertEqual(expected_results, json.loads(result.output)) def test_package_list_keyword(self): - item1 = {'name': 'package1', 'keyName': 'PACKAGE1', 'type': {'keyName': 'BARE_METAL_CPU'}, 'isActive': 1} - item2 = {'name': 'package2', 'keyName': 'PACKAGE2', 'type': {'keyName': 'BARE_METAL_CPU'}, 'isActive': 1} p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - p_mock.return_value = [item1, item2] + p_mock.return_value = _get_all_packages() _filter = {'type': {'keyName': {'operation': '!= BLUEMIX_SERVICE'}}} _filter['name'] = {'operation': '*= package1'} @@ -74,23 +69,21 @@ self.assert_no_fail(result) self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects', filter=_filter) - expected_results = [{'name': 'package1', 'keyName': 'PACKAGE1', 'type': 'BARE_METAL_CPU'}, - {'name': 'package2', 'keyName': 'PACKAGE2', 'type': 'BARE_METAL_CPU'}] + expected_results = [{'id': 1, 'name': 'package1', 'keyName': 'PACKAGE1', 'type': 'BARE_METAL_CPU'}, + {'id': 2, 'name': 'package2', 'keyName': 'PACKAGE2', 'type': 'BARE_METAL_CPU'}] self.assertEqual(expected_results, json.loads(result.output)) def test_package_list_type(self): - item1 = {'name': 'package1', 'keyName': 'PACKAGE1', 'type': {'keyName': 'BARE_METAL_CPU'}, 'isActive': 1} - item2 = {'name': 'package2', 'keyName': 'PACKAGE2', 'type': {'keyName': 'BARE_METAL_CPU'}, 'isActive': 1} p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - p_mock.return_value = [item1, item2] + p_mock.return_value = _get_all_packages() _filter = {'type': {'keyName': {'operation': 'BARE_METAL_CPU'}}} result = self.run_command(['order', 'package-list', '--package_type', 'BARE_METAL_CPU']) self.assert_no_fail(result) self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects', filter=_filter) - expected_results = [{'name': 'package1', 'keyName': 'PACKAGE1', 'type': 'BARE_METAL_CPU'}, - {'name': 'package2', 'keyName': 'PACKAGE2', 'type': 'BARE_METAL_CPU'}] + expected_results = [{'id': 1, 'name': 'package1', 'keyName': 'PACKAGE1', 'type': 'BARE_METAL_CPU'}, + {'id': 2, 'name': 'package2', 'keyName': 'PACKAGE2', 'type': 'BARE_METAL_CPU'}] self.assertEqual(expected_results, json.loads(result.output)) def test_place(self): @@ -114,6 +107,35 @@ 'status': 'APPROVED'}, json.loads(result.output)) + def test_place_quote(self): + order_date = '2018-04-04 07:39:20' + expiration_date = '2018-05-04 07:39:20' + quote_name = 'foobar' + order = {'orderDate': order_date, + 'quote': { + 'id': 1234, + 'name': quote_name, + 'expirationDate': expiration_date, + 'status': 'PENDING' + }} + place_quote_mock = self.set_mock('SoftLayer_Product_Order', 'placeQuote') + items_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + + place_quote_mock.return_value = order + items_mock.return_value = self._get_order_items() + + result = self.run_command(['order', 'place-quote', '--name', 'foobar', 'package', 'DALLAS13', + 'ITEM1', '--complex-type', 'SoftLayer_Container_Product_Order_Thing']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'placeQuote') + self.assertEqual({'id': 1234, + 'name': quote_name, + 'created': order_date, + 'expires': expiration_date, + 'status': 'PENDING'}, + json.loads(result.output)) + def test_verify_hourly(self): order_date = '2017-04-04 07:39:20' order = {'orderId': 1234, 'orderDate': order_date, @@ -133,7 +155,7 @@ self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') expected_results = [] - for price in order['prices']: + for price in order['orderContainers'][0]['prices']: expected_results.append({'keyName': price['item']['keyName'], 'description': price['item']['description'], 'cost': price['hourlyRecurringFee']}) @@ -160,7 +182,7 @@ self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') expected_results = [] - for price in order['prices']: + for price in order['orderContainers'][0]['prices']: expected_results.append({'keyName': price['item']['keyName'], 'description': price['item']['description'], 'cost': price['recurringFee']}) @@ -210,9 +232,13 @@ def _get_order_items(self): item1 = {'keyName': 'ITEM1', 'description': 'description1', - 'prices': [{'id': 1111, 'locationGroupId': None}]} + 'itemCategory': {'categoryCode': 'cat1'}, + 'prices': [{'id': 1111, 'locationGroupId': None, + 'categories': [{'categoryCode': 'cat1'}]}]} item2 = {'keyName': 'ITEM2', 'description': 'description2', - 'prices': [{'id': 2222, 'locationGroupId': None}]} + 'itemCategory': {'categoryCode': 'cat2'}, + 'prices': [{'id': 2222, 'locationGroupId': None, + 'categories': [{'categoryCode': 'cat2'}]}]} return [item1, item2] @@ -222,4 +248,14 @@ 'recurringFee': '120'} price2 = {'item': item2, 'hourlyRecurringFee': '0.05', 'recurringFee': '150'} - return {'prices': [price1, price2]} + return {'orderContainers': [{'prices': [price1, price2]}]} + + +def _get_all_packages(): + package_type = {'keyName': 'BARE_METAL_CPU'} + all_packages = [ + {'id': 1, 'name': 'package1', 'keyName': 'PACKAGE1', 'type': package_type, 'isActive': 1}, + {'id': 2, 'name': 'package2', 'keyName': 'PACKAGE2', 'type': package_type, 'isActive': 1}, + {'id': 3, 'name': 'package2', 'keyName': 'PACKAGE2', 'type': package_type, 'isActive': 0} + ] + return all_packages diff -Nru python-softlayer-5.4.4/tests/CLI/modules/securitygroup_tests.py python-softlayer-5.6.4/tests/CLI/modules/securitygroup_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/securitygroup_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/securitygroup_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -118,7 +118,9 @@ 'remoteGroupId': None, 'protocol': None, 'portRangeMin': None, - 'portRangeMax': None}], + 'portRangeMax': None, + 'createDate': None, + 'modifyDate': None}], json.loads(result.output)) def test_securitygroup_rule_add(self): diff -Nru python-softlayer-5.4.4/tests/CLI/modules/server_tests.py python-softlayer-5.6.4/tests/CLI/modules/server_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/server_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/server_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -26,6 +26,75 @@ self.assert_no_fail(result) self.assertEqual(len(output), 10) + def test_server_credentials(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = { + "accountId": 11111, + "domain": "chechu.com", + "fullyQualifiedDomainName": "host3.vmware.chechu.com", + "hardwareStatusId": 5, + "hostname": "host3.vmware", + "id": 12345, + "softwareComponents": [{"passwords": [ + { + "password": "abc123", + "username": "root" + } + ]}] + } + result = self.run_command(['hardware', 'credentials', '12345']) + + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), + [{ + 'username': 'root', + 'password': 'abc123' + }]) + + def test_server_credentials_exception_passwords_not_found(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = { + "accountId": 11111, + "domain": "chechu.com", + "fullyQualifiedDomainName": "host3.vmware.chechu.com", + "hardwareStatusId": 5, + "hostname": "host3.vmware", + "id": 12345, + "softwareComponents": [{}] + } + + result = self.run_command(['hardware', 'credentials', '12345']) + + self.assertEqual( + 'No passwords found in softwareComponents', + str(result.exception) + ) + + def test_server_credentials_exception_password_not_found(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = { + "accountId": 11111, + "domain": "chechu.com", + "fullyQualifiedDomainName": "host3.vmware.chechu.com", + "hardwareStatusId": 5, + "hostname": "host3.vmware", + "id": 12345, + "softwareComponents": [ + { + "hardwareId": 22222, + "id": 333333, + "passwords": [{}] + } + ] + } + + result = self.run_command(['hardware', 'credentials', '12345']) + + self.assertEqual( + 'None', + str(result.exception) + ) + def test_server_details(self): result = self.run_command(['server', 'detail', '1234', '--passwords', '--price']) @@ -313,7 +382,7 @@ @mock.patch('SoftLayer.CLI.template.export_to_template') def test_create_server_with_export(self, export_mock): - if(sys.platform.startswith("win")): + if (sys.platform.startswith("win")): self.skipTest("Test doesn't work in Windows") result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', @@ -387,7 +456,7 @@ hostname='hardware-test1') def test_edit_server_userfile(self): - if(sys.platform.startswith("win")): + if (sys.platform.startswith("win")): self.skipTest("Test doesn't work in Windows") with tempfile.NamedTemporaryFile() as userfile: userfile.write(b"some data") diff -Nru python-softlayer-5.4.4/tests/CLI/modules/subnet_tests.py python-softlayer-5.6.4/tests/CLI/modules/subnet_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/subnet_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/subnet_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -35,3 +35,7 @@ 'usable ips': 22 }, json.loads(result.output)) + + def test_list(self): + result = self.run_command(['subnet', 'list']) + self.assert_no_fail(result) diff -Nru python-softlayer-5.4.4/tests/CLI/modules/ticket_tests.py python-softlayer-5.6.4/tests/CLI/modules/ticket_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/ticket_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/ticket_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -8,6 +8,9 @@ import mock from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import ticket +from SoftLayer.managers import TicketManager from SoftLayer import testing @@ -20,10 +23,12 @@ 'assigned_user': 'John Smith', 'id': 102, 'last_edited': '2013-08-01T14:16:47-07:00', + 'priority': 0, 'status': 'Open', - 'title': 'Cloud Instance Cancellation - 08/01/13'}] + 'title': 'Cloud Instance Cancellation - 08/01/13', + 'updates': 0}] self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), expected) + self.assertEqual(expected, json.loads(result.output)) def test_detail(self): result = self.run_command(['ticket', 'detail', '1']) @@ -32,6 +37,7 @@ 'created': '2013-08-01T14:14:04-07:00', 'edited': '2013-08-01T14:16:47-07:00', 'id': 100, + 'priority': 'No Priority', 'status': 'Closed', 'title': 'Cloud Instance Cancellation - 08/01/13', 'update 1': 'a bot says something', @@ -53,8 +59,23 @@ 'assignedUserId': 12345, 'title': 'Test'}, 'ticket body') - self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', - args=args) + self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', args=args) + + def test_create_with_priority(self): + result = self.run_command(['ticket', 'create', '--title=Test', + '--subject-id=1000', + '--body=ticket body', + '--priority=1']) + + self.assert_no_fail(result) + + args = ({'subjectId': 1000, + 'contents': 'ticket body', + 'assignedUserId': 12345, + 'title': 'Test', + 'priority': 1}, 'ticket body') + + self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', args=args) def test_create_and_attach(self): result = self.run_command(['ticket', 'create', '--title=Test', @@ -204,3 +225,79 @@ args=({"filename": "a_file_name", "data": b"ticket attached data"},), identifier=1) + + def test_init_ticket_results(self): + ticket_mgr = TicketManager(self.client) + ticket_table = ticket.get_ticket_results(ticket_mgr, 100) + self.assert_called_with('SoftLayer_Ticket', 'getObject', identifier=100) + self.assertIsInstance(ticket_table, formatting.KeyValueTable) + + ticket_object = ticket_table.to_python() + self.assertEqual('No Priority', ticket_object['priority']) + self.assertEqual(100, ticket_object['id']) + + def test_init_ticket_results_asigned_user(self): + mock = self.set_mock('SoftLayer_Ticket', 'getObject') + mock.return_value = { + "id": 100, + "title": "Simple Title", + "priority": 1, + "assignedUser": { + "firstName": "Test", + "lastName": "User" + }, + "status": { + "name": "Closed" + }, + "createDate": "2013-08-01T14:14:04-07:00", + "lastEditDate": "2013-08-01T14:16:47-07:00", + "updates": [{'entry': 'a bot says something'}] + } + + ticket_mgr = TicketManager(self.client) + ticket_table = ticket.get_ticket_results(ticket_mgr, 100) + self.assert_called_with('SoftLayer_Ticket', 'getObject', identifier=100) + self.assertIsInstance(ticket_table, formatting.KeyValueTable) + + ticket_object = ticket_table.to_python() + self.assertEqual('Severity 1 - Critical Impact / Service Down', ticket_object['priority']) + self.assertEqual('Test User', ticket_object['user']) + + def test_ticket_summary(self): + mock = self.set_mock('SoftLayer_Account', 'getObject') + mock.return_value = { + 'openTicketCount': 1, + 'closedTicketCount': 2, + 'openBillingTicketCount': 3, + 'openOtherTicketCount': 4, + 'openSalesTicketCount': 5, + 'openSupportTicketCount': 6, + 'openAccountingTicketCount': 7 + } + expected = [ + {'Status': 'Open', + 'count': [ + {'Type': 'Accounting', 'count': 7}, + {'Type': 'Billing', 'count': 3}, + {'Type': 'Sales', 'count': 5}, + {'Type': 'Support', 'count': 6}, + {'Type': 'Other', 'count': 4}, + {'Type': 'Total', 'count': 1}]}, + {'Status': 'Closed', 'count': 2} + ] + result = self.run_command(['ticket', 'summary']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getObject') + self.assertEqual(expected, json.loads(result.output)) + + def test_ticket_update(self): + result = self.run_command(['ticket', 'update', '100', '--body=Testing']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Ticket', 'addUpdate', args=({'entry': 'Testing'},), identifier=100) + + @mock.patch('click.edit') + def test_ticket_update_no_body(self, edit_mock): + edit_mock.return_value = 'Testing1' + result = self.run_command(['ticket', 'update', '100']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Ticket', 'addUpdate', args=({'entry': 'Testing1'},), identifier=100) diff -Nru python-softlayer-5.4.4/tests/CLI/modules/user_tests.py python-softlayer-5.6.4/tests/CLI/modules/user_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/user_tests.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/user_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,271 @@ +""" + SoftLayer.tests.CLI.modules.user_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests for the user cli command +""" +import json +import sys + +import mock +import testtools + +from SoftLayer import testing + + +class UserCLITests(testing.TestCase): + + """User list tests""" + + def test_user_list(self): + result = self.run_command(['user', 'list']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getUsers') + + def test_user_list_only_id(self): + result = self.run_command(['user', 'list', '--columns=id']) + self.assert_no_fail(result) + self.assertEqual([{"id": 11100}, {"id": 11111}], json.loads(result.output)) + + """User detail tests""" + + def test_detail(self): + result = self.run_command(['user', 'detail', '11100']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'getObject') + + def test_detail_keys(self): + result = self.run_command(['user', 'detail', '11100', '-k']) + self.assert_no_fail(result) + self.assertIn('APIKEY', result.output) + + def test_detail_permissions(self): + result = self.run_command(['user', 'detail', '11100', '-p']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'getPermissions') + self.assertIn('ACCESS_ALL_HARDWARE', result.output) + + def test_detail_hardware(self): + result = self.run_command(['user', 'detail', '11100', '-h']) + self.assert_no_fail(result) + self.assert_called_with( + 'SoftLayer_User_Customer', 'getObject', identifier=11100, + mask='mask[id, hardware, dedicatedHosts]' + ) + + def test_detail_virtual(self): + result = self.run_command(['user', 'detail', '11100', '-v']) + self.assert_no_fail(result) + self.assert_called_with( + 'SoftLayer_User_Customer', 'getObject', identifier=11100, + mask='mask[id, virtualGuests]' + ) + + def test_detail_logins(self): + result = self.run_command(['user', 'detail', '11100', '-l']) + self.assert_no_fail(result) + self.assert_called_with( + 'SoftLayer_User_Customer', 'getLoginAttempts', identifier=11100 + ) + + def test_detail_events(self): + result = self.run_command(['user', 'detail', '11100', '-e']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects') + + def test_print_hardware_access(self): + mock = self.set_mock('SoftLayer_User_Customer', 'getObject') + mock.return_value = { + 'accountId': 12345, + 'address1': '315 Test Street', + 'city': 'Houston', + 'companyName': 'SoftLayer Development Community', + 'country': 'US', + 'displayName': 'Test', + 'email': 'test@us.ibm.com', + 'firstName': 'Test', + 'id': 244956, + 'lastName': 'Testerson', + 'postalCode': '77002', + 'state': 'TX', + 'statusDate': None, + 'hardware': [ + {'id': 1234, + 'fullyQualifiedDomainName': 'test.test.test', + 'provisionDate': '2018-05-08T15:28:32-06:00', + 'primaryBackendIpAddress': '175.125.126.118', + 'primaryIpAddress': '175.125.126.118'} + ], + 'dedicatedHosts': [ + {'id': 1234, + 'fullyQualifiedDomainName': 'test.test.test', + 'provisionDate': '2018-05-08T15:28:32-06:00', + 'primaryBackendIpAddress': '175.125.126.118', + 'primaryIpAddress': '175.125.126.118'} + ], + } + result = self.run_command(['user', 'detail', '11100', '-h']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'getObject', identifier=11100, + mask="mask[id, hardware, dedicatedHosts]") + + """User permissions tests""" + + def test_permissions_list(self): + result = self.run_command(['user', 'permissions', '11100']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer_CustomerPermission_Permission', 'getAllObjects') + self.assert_called_with( + 'SoftLayer_User_Customer', 'getObject', identifier=11100, + mask='mask[id, permissions, isMasterUserFlag, roles]' + ) + + """User edit-permissions tests""" + + def test_edit_perms_on(self): + result = self.run_command(['user', 'edit-permissions', '11100', '--enable', '-p', 'TEST']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'addBulkPortalPermission', identifier=11100) + + def test_edit_perms_on_bad(self): + result = self.run_command(['user', 'edit-permissions', '11100', '--enable', '-p', 'TEST_NOt_exist']) + self.assertEqual(result.exit_code, 1) + + def test_edit_perms_off(self): + result = self.run_command(['user', 'edit-permissions', '11100', '--disable', '-p', 'TEST']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'removeBulkPortalPermission', identifier=11100) + + @mock.patch('SoftLayer.CLI.user.edit_permissions.click') + def test_edit_perms_off_failure(self, click): + permission_mock = self.set_mock('SoftLayer_User_Customer', 'removeBulkPortalPermission') + permission_mock.return_value = False + result = self.run_command(['user', 'edit-permissions', '11100', '--disable', '-p', 'TEST']) + click.secho.assert_called_with('Failed to update permissions: TEST', fg='red') + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'removeBulkPortalPermission', identifier=11100) + + def test_edit_perms_from_user(self): + result = self.run_command(['user', 'edit-permissions', '11100', '-u', '1234']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'getPermissions', identifier=1234) + self.assert_called_with('SoftLayer_User_Customer', 'removeBulkPortalPermission', identifier=11100) + self.assert_called_with('SoftLayer_User_Customer', 'addBulkPortalPermission', identifier=11100) + + """User create tests""" + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', '-p', 'testword']) + self.assert_no_fail(result) + self.assertIn('test@us.ibm.com', result.output) + self.assert_called_with('SoftLayer_Account', 'getCurrentUser') + self.assert_called_with('SoftLayer_User_Customer', 'createObject', args=mock.ANY) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_no_confirm(self, confirm_mock): + confirm_mock.return_value = False + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', '-p', 'testword']) + self.assertEqual(result.exit_code, 2) + + @testtools.skipIf(sys.version_info < (3, 6), "Secrets module only exists in version 3.6+") + @mock.patch('secrets.choice') + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_generate_password_36(self, confirm_mock, secrets): + secrets.return_value = 'Q' + confirm_mock.return_value = True + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', '-p', 'generate']) + + self.assert_no_fail(result) + self.assertIn('test@us.ibm.com', result.output) + self.assertIn('QQQQQQQQQQQQQQQQQQQQQQ', result.output) + self.assert_called_with('SoftLayer_Account', 'getCurrentUser') + self.assert_called_with('SoftLayer_User_Customer', 'createObject', args=mock.ANY) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_generate_password_2(self, confirm_mock): + if sys.version_info >= (3, 6): + self.skipTest("Python needs to be < 3.6 for this test.") + + confirm_mock.return_value = True + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', '-p', 'generate']) + self.assertIn(result.output, "ImportError") + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_and_apikey(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', '-a']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'addApiAuthenticationKey') + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_with_template(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', + '-t', '{"firstName": "Supermand"}']) + self.assertIn('Supermand', result.output) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_with_bad_template(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', + '-t', '{firstName: "Supermand"}']) + self.assertIn("Argument Error", result.exception.message) + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_with_no_confirm(self, confirm_mock): + confirm_mock.return_value = False + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com']) + self.assertIn("Canceling creation!", result.exception.message) + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_user_from_user(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['user', 'create', 'test', '-e', 'test@us.ibm.com', '-u', '1234']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'getObject', identifier=1234) + + """User edit-details tests""" + @mock.patch('SoftLayer.CLI.user.edit_details.click') + def test_edit_details(self, click): + result = self.run_command(['user', 'edit-details', '1234', '-t', '{"firstName":"Supermand"}']) + click.secho.assert_called_with('1234 updated successfully', fg='green') + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'editObject', + args=({'firstName': 'Supermand'},), identifier=1234) + + @mock.patch('SoftLayer.CLI.user.edit_details.click') + def test_edit_details_failure(self, click): + mock = self.set_mock('SoftLayer_User_Customer', 'editObject') + mock.return_value = False + result = self.run_command(['user', 'edit-details', '1234', '-t', '{"firstName":"Supermand"}']) + click.secho.assert_called_with('Failed to update 1234', fg='red') + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'editObject', + args=({'firstName': 'Supermand'},), identifier=1234) + + def test_edit_details_bad_json(self): + result = self.run_command(['user', 'edit-details', '1234', '-t', '{firstName:"Supermand"}']) + self.assertIn("Argument Error", result.exception.message) + self.assertEqual(result.exit_code, 2) + + """User delete tests""" + @mock.patch('SoftLayer.CLI.user.delete.click') + def test_delete(self, click): + result = self.run_command(['user', 'delete', '12345']) + click.secho.assert_called_with('12345 deleted successfully', fg='green') + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'editObject', + args=({'userStatusId': 1021},), identifier=12345) + + @mock.patch('SoftLayer.CLI.user.delete.click') + def test_delete_failure(self, click): + mock = self.set_mock('SoftLayer_User_Customer', 'editObject') + mock.return_value = False + result = self.run_command(['user', 'delete', '12345']) + click.secho.assert_called_with('Failed to delete 12345', fg='red') + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_User_Customer', 'editObject', + args=({'userStatusId': 1021},), identifier=12345) diff -Nru python-softlayer-5.4.4/tests/CLI/modules/vlan_tests.py python-softlayer-5.6.4/tests/CLI/modules/vlan_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/vlan_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/vlan_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -47,3 +47,32 @@ vlan_mock.return_value = getObject result = self.run_command(['vlan', 'detail', '1234']) self.assert_no_fail(result) + + def test_detail_hardware_without_hostname(self): + vlan_mock = self.set_mock('SoftLayer_Network_Vlan', 'getObject') + getObject = { + 'primaryRouter': { + 'datacenter': {'id': 1234, 'longName': 'TestDC'}, + 'fullyQualifiedDomainName': 'fcr01.TestDC' + }, + 'id': 1234, + 'vlanNumber': 4444, + 'firewallInterfaces': None, + 'subnets': [], + 'hardware': [ + {'a_hardware': 'that_has_none_of_the_expected_attributes_provided'}, + {'domain': 'example.com', + 'networkManagementIpAddress': '10.171.202.131', + 'hardwareStatus': {'status': 'ACTIVE', 'id': 5}, + 'notes': '', + 'hostname': 'hw1', 'hardwareStatusId': 5, + 'globalIdentifier': 'f6ea716a-41d8-4c52-bb2e-48d63105f4b0', + 'primaryIpAddress': '169.60.169.169', + 'primaryBackendIpAddress': '10.171.202.130', 'id': 826425, + 'privateIpAddress': '10.171.202.130', + 'fullyQualifiedDomainName': 'hw1.example.com'} + ] + } + vlan_mock.return_value = getObject + result = self.run_command(['vlan', 'detail', '1234']) + self.assert_no_fail(result) diff -Nru python-softlayer-5.4.4/tests/CLI/modules/vs_capacity_tests.py python-softlayer-5.6.4/tests/CLI/modules/vs_capacity_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/vs_capacity_tests.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/vs_capacity_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,71 @@ +""" + SoftLayer.tests.CLI.modules.vs_capacity_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +from SoftLayer.fixtures import SoftLayer_Product_Order +from SoftLayer.fixtures import SoftLayer_Product_Package +from SoftLayer import testing + + +class VSCapacityTests(testing.TestCase): + + def test_list(self): + result = self.run_command(['vs', 'capacity', 'list']) + self.assert_no_fail(result) + + def test_detail(self): + result = self.run_command(['vs', 'capacity', 'detail', '1234']) + self.assert_no_fail(result) + + def test_detail_pending(self): + # Instances don't have a billing item if they haven't been approved yet. + capacity_mock = self.set_mock('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject') + get_object = { + 'name': 'test-capacity', + 'instances': [ + { + 'createDate': '2018-09-24T16:33:09-06:00', + 'guestId': 62159257, + 'id': 3501, + } + ] + } + capacity_mock.return_value = get_object + result = self.run_command(['vs', 'capacity', 'detail', '1234']) + self.assert_no_fail(result) + + def test_create_test(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + order_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + order_mock.return_value = SoftLayer_Product_Order.rsc_verifyOrder + result = self.run_command(['vs', 'capacity', 'create', '--name=TEST', '--test', + '--backend_router_id=1234', '--flavor=B1_1X2_1_YEAR_TERM', '--instances=10']) + self.assert_no_fail(result) + + def test_create(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + order_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + order_mock.return_value = SoftLayer_Product_Order.rsc_placeOrder + result = self.run_command(['vs', 'capacity', 'create', '--name=TEST', '--instances=10', + '--backend_router_id=1234', '--flavor=B1_1X2_1_YEAR_TERM']) + self.assert_no_fail(result) + + def test_create_options(self): + result = self.run_command(['vs', 'capacity', 'create_options']) + self.assert_no_fail(result) + + def test_create_guest_test(self): + result = self.run_command(['vs', 'capacity', 'create-guest', '--capacity-id=3103', '--primary-disk=25', + '-H ABCDEFG', '-D test_list.com', '-o UBUNTU_LATEST_64', '-kTest 1', '--test']) + self.assert_no_fail(result) + + def test_create_guest(self): + order_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + order_mock.return_value = SoftLayer_Product_Order.rsi_placeOrder + result = self.run_command(['vs', 'capacity', 'create-guest', '--capacity-id=3103', '--primary-disk=25', + '-H ABCDEFG', '-D test_list.com', '-o UBUNTU_LATEST_64', '-kTest 1']) + self.assert_no_fail(result) diff -Nru python-softlayer-5.4.4/tests/CLI/modules/vs_tests.py python-softlayer-5.6.4/tests/CLI/modules/vs_tests.py --- python-softlayer-5.4.4/tests/CLI/modules/vs_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/CLI/modules/vs_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -15,6 +15,127 @@ class VirtTests(testing.TestCase): + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_rescue_vs(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'rescue', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_rescue_vs_no_confirm(self, confirm_mock): + confirm_mock.return_value = False + result = self.run_command(['vs', 'rescue', '100']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_reboot_vs_default(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'rebootDefault') + mock.return_value = 'true' + confirm_mock.return_value = True + result = self.run_command(['vs', 'reboot', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_reboot_vs_no_confirm(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'rebootDefault') + mock.return_value = 'true' + confirm_mock.return_value = False + result = self.run_command(['vs', 'reboot', '100']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_reboot_vs_soft(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'rebootSoft') + mock.return_value = 'true' + confirm_mock.return_value = True + + result = self.run_command(['vs', 'reboot', '--soft', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_reboot_vs_hard(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'rebootHard') + mock.return_value = 'true' + confirm_mock.return_value = True + result = self.run_command(['vs', 'reboot', '--hard', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_power_vs_off_soft(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'powerOffSoft') + mock.return_value = 'true' + confirm_mock.return_value = True + + result = self.run_command(['vs', 'power-off', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_power_off_vs_no_confirm(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'powerOffSoft') + mock.return_value = 'true' + confirm_mock.return_value = False + + result = self.run_command(['vs', 'power-off', '100']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_power_off_vs_hard(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'powerOff') + mock.return_value = 'true' + confirm_mock.return_value = True + + result = self.run_command(['vs', 'power-off', '--hard', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_power_on_vs(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'powerOn') + mock.return_value = 'true' + confirm_mock.return_value = True + + result = self.run_command(['vs', 'power-on', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_pause_vs(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'pause') + mock.return_value = 'true' + confirm_mock.return_value = True + + result = self.run_command(['vs', 'pause', '100']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_pause_vs_no_confirm(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'pause') + mock.return_value = 'true' + confirm_mock.return_value = False + + result = self.run_command(['vs', 'pause', '100']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_resume_vs(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'resume') + mock.return_value = 'true' + confirm_mock.return_value = True + + result = self.run_command(['vs', 'resume', '100']) + + self.assert_no_fail(result) + def test_list_vs(self): result = self.run_command(['vs', 'list', '--tag=tag']) @@ -33,6 +154,54 @@ 'id': 104, 'backend_ip': '10.45.19.35'}]) + @mock.patch('SoftLayer.utils.lookup') + def test_detail_vs_empty_billing(self, mock_lookup): + def mock_lookup_func(dic, key, *keys): + if key == 'billingItem': + return [] + if keys: + return mock_lookup_func(dic.get(key, {}), keys[0], *keys[1:]) + return dic.get(key) + + mock_lookup.side_effect = mock_lookup_func + + result = self.run_command(['vs', 'detail', '100', '--passwords', '--price']) + + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), + {'active_transaction': None, + 'cores': 2, + 'created': '2013-08-01 15:23:45', + 'datacenter': 'TEST00', + 'dedicated_host': 'test-dedicated', + 'dedicated_host_id': 37401, + 'hostname': 'vs-test1', + 'domain': 'test.sftlyr.ws', + 'fqdn': 'vs-test1.test.sftlyr.ws', + 'id': 100, + 'guid': '1a2b3c-1701', + 'memory': 1024, + 'modified': {}, + 'os': 'Ubuntu', + 'os_version': '12.04-64 Minimal for VSI', + 'notes': 'notes', + 'price_rate': 0, + 'tags': ['production'], + 'private_cpu': {}, + 'private_ip': '10.45.19.37', + 'private_only': {}, + 'ptr': 'test.softlayer.com.', + 'public_ip': '172.16.240.2', + 'state': 'RUNNING', + 'status': 'ACTIVE', + 'users': [{'software': 'Ubuntu', + 'password': 'pass', + 'username': 'user'}], + 'vlans': [{'type': 'PUBLIC', + 'number': 23, + 'id': 1}], + 'owner': None}) + def test_detail_vs(self): result = self.run_command(['vs', 'detail', '100', '--passwords', '--price']) @@ -136,6 +305,7 @@ @mock.patch('SoftLayer.CLI.formatting.confirm') def test_create(self, confirm_mock): confirm_mock.return_value = True + result = self.run_command(['vs', 'create', '--cpu=2', '--domain=example.com', @@ -168,6 +338,77 @@ args=args) @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_vlan_subnet(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', + '--cpu=2', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--memory=1', + '--billing=hourly', + '--datacenter=dal05', + '--vlan-private=577940', + '--subnet-private=478700', + '--vlan-public=1639255', + '--subnet-public=297614', + '--tag=dev', + '--tag=green']) + + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), + {'guid': '1a2b3c-1701', + 'id': 100, + 'created': '2013-08-01 15:23:45'}) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_wait_ready(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + "provisionDate": "2018-06-10T12:00:00-05:00", + "id": 100 + } + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', + '--cpu=2', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--memory=1', + '--network=100', + '--billing=hourly', + '--datacenter=dal05', + '--wait=1']) + + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_wait_not_ready(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + "ready": False, + "guid": "1a2b3c-1701", + "id": 100, + "created": "2018-06-10 12:00:00" + } + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', + '--cpu=2', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--memory=1', + '--network=100', + '--billing=hourly', + '--datacenter=dal05', + '--wait=1']) + + self.assertEqual(result.exit_code, 1) + + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_create_with_integer_image_id(self, confirm_mock): confirm_mock.return_value = True result = self.run_command(['vs', 'create', @@ -237,6 +478,66 @@ args=args) @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_flavor_and_memory(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--network=100', + '--datacenter=TEST00', + '--flavor=BL_1X2X25', + '--memory=2048MB']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_dedicated_and_flavor(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--network=100', + '--datacenter=TEST00', + '--dedicated', + '--flavor=BL_1X2X25']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_hostid_and_flavor(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--network=100', + '--datacenter=dal05', + '--host-id=100', + '--flavor=BL_1X2X25']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_flavor_and_cpu(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--network=100', + '--datacenter=TEST00', + '--flavor=BL_1X2X25', + '--cpu=2']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_create_with_host_id(self, confirm_mock): confirm_mock.return_value = True result = self.run_command(['vs', 'create', @@ -363,6 +664,36 @@ args=args) @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_vs_test(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', '--test', '--hostname', 'TEST', + '--domain', 'TESTING', '--cpu', '1', + '--memory', '2048MB', '--datacenter', + 'TEST00', '--os', 'UBUNTU_LATEST']) + + self.assertEqual(result.exit_code, 0) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_vs_flavor_test(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', '--test', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', 'B1_2X8X25', + '--datacenter', 'TEST00', '--os', 'UBUNTU_LATEST']) + + self.assert_no_fail(result) + self.assertEqual(result.exit_code, 0) + + def test_create_vs_bad_memory(self): + result = self.run_command(['vs', 'create', '--hostname', 'TEST', + '--domain', 'TESTING', '--cpu', '1', + '--memory', '2034MB', '--flavor', + 'UBUNTU', '--datacenter', 'TEST00']) + + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_dns_sync_both(self, confirm_mock): confirm_mock.return_value = True getReverseDomainRecords = self.set_mock('SoftLayer_Virtual_Guest', @@ -581,6 +912,23 @@ self.assertIn({'id': 1122}, order_container['prices']) self.assertEqual(order_container['virtualGuests'], [{'id': 100}]) + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_upgrade_with_flavor(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'upgrade', '100', '--flavor=M1_64X512X100']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + call = self.calls('SoftLayer_Product_Order', 'placeOrder')[0] + order_container = call.args[0] + self.assertEqual(799, order_container['presetId']) + self.assertIn({'id': 100}, order_container['virtualGuests']) + self.assertEqual(order_container['virtualGuests'], [{'id': 100}]) + + def test_upgrade_with_cpu_memory_and_flavor(self): + result = self.run_command(['vs', 'upgrade', '100', '--cpu=4', + '--memory=1024', '--flavor=M1_64X512X100']) + self.assertEqual("Do not use cpu, private and memory if you are using flavors", str(result.exception)) + def test_edit(self): result = self.run_command(['vs', 'edit', '--domain=example.com', @@ -662,3 +1010,35 @@ result = self.run_command(['vs', 'ready', '100', '--wait=100']) self.assert_no_fail(result) self.assertEqual(result.output, '"READY"\n') + + @mock.patch('SoftLayer.CLI.formatting.no_going_back') + def test_reload(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'reloadCurrentOperatingSystemConfguration') + confirm_mock.return_value = True + mock.return_value = 'true' + + result = self.run_command(['vs', 'reload', '--postinstall', '100', '--key', '100', '--image', '100', '100']) + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.no_going_back') + def test_reload_no_confirm(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'reloadCurrentOperatingSystemConfiguration') + confirm_mock.return_value = False + mock.return_value = 'false' + + result = self.run_command(['vs', 'reload', '--postinstall', '100', '--key', '100', '--image', '100', '100']) + self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.CLI.formatting.no_going_back') + def test_cancel(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'cancel', '100']) + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.no_going_back') + def test_cancel_no_confirm(self, confirm_mock): + confirm_mock.return_value = False + + result = self.run_command(['vs', 'cancel', '100']) + self.assertEqual(result.exit_code, 2) diff -Nru python-softlayer-5.4.4/tests/config_tests.py python-softlayer-5.6.4/tests/config_tests.py --- python-softlayer-5.4.4/tests/config_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/config_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -92,4 +92,4 @@ config.get_client_settings_config_file(config_file='path/to/config') config_parser().read.assert_called_with([mock.ANY, mock.ANY, - 'path/to/config']) + 'path/to/config']) diff -Nru python-softlayer-5.4.4/tests/managers/block_tests.py python-softlayer-5.6.4/tests/managers/block_tests.py --- python-softlayer-5.4.4/tests/managers/block_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/block_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -41,33 +41,72 @@ identifier=449, ) + def test_cancel_block_volume_exception_billing_item_not_found(self): + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = { + 'capacityGb': 20, + 'createDate': '2017-06-20T14:24:21-06:00', + 'nasType': 'ISCSI', + 'storageTypeId': '7', + 'serviceResourceName': 'PerfStor Aggr aggr_staasdal0601_pc01' + } + exception = self.assertRaises( + exceptions.SoftLayerError, + self.block.cancel_block_volume, + 12345, + immediate=True + ) + self.assertEqual( + 'Block Storage was already cancelled', + str(exception) + ) + + def test_cancel_block_volume_billing_item_found(self): + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = { + 'capacityGb': 20, + 'createDate': '2017-06-20T14:24:21-06:00', + 'nasType': 'ISCSI', + 'storageTypeId': '7', + 'serviceResourceName': 'PerfStor Aggr aggr_staasdal0601_pc01', + 'billingItem': {'hourlyFlag': True, 'id': 449}, + } + self.block.cancel_block_volume(123, immediate=False) + + self.assert_called_with( + 'SoftLayer_Billing_Item', + 'cancelItem', + args=(True, True, 'No longer needed'), + identifier=449, + ) + def test_get_block_volume_details(self): result = self.block.get_block_volume_details(100) self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, result) - expected_mask = 'id,'\ - 'username,'\ - 'password,'\ - 'capacityGb,'\ - 'snapshotCapacityGb,'\ - 'parentVolume.snapshotSizeBytes,'\ - 'storageType.keyName,'\ - 'serviceResource.datacenter[name],'\ - 'serviceResourceBackendIpAddress,'\ - 'storageTierLevel,'\ - 'provisionedIops,'\ - 'lunId,'\ - 'originalVolumeName,'\ - 'originalSnapshotName,'\ - 'originalVolumeSize,'\ - 'activeTransactionCount,'\ - 'activeTransactions.transactionStatus[friendlyName],'\ - 'replicationPartnerCount,'\ - 'replicationStatus,'\ - 'replicationPartners[id,username,'\ - 'serviceResourceBackendIpAddress,'\ - 'serviceResource[datacenter[name]],'\ + expected_mask = 'id,' \ + 'username,' \ + 'password,' \ + 'capacityGb,' \ + 'snapshotCapacityGb,' \ + 'parentVolume.snapshotSizeBytes,' \ + 'storageType.keyName,' \ + 'serviceResource.datacenter[name],' \ + 'serviceResourceBackendIpAddress,' \ + 'storageTierLevel,' \ + 'provisionedIops,' \ + 'lunId,' \ + 'originalVolumeName,' \ + 'originalSnapshotName,' \ + 'originalVolumeSize,' \ + 'activeTransactionCount,' \ + 'activeTransactions.transactionStatus[friendlyName],' \ + 'replicationPartnerCount,' \ + 'replicationStatus,' \ + 'replicationPartners[id,username,' \ + 'serviceResourceBackendIpAddress,' \ + 'serviceResource[datacenter[name]],' \ 'replicationSchedule[type[keyname]]]' self.assert_called_with( @@ -75,7 +114,7 @@ 'getObject', identifier=100, mask='mask[%s]' % expected_mask - ) + ) def test_list_block_volumes(self): result = self.block.list_block_volumes() @@ -96,14 +135,14 @@ } } - expected_mask = 'id,'\ - 'username,'\ - 'lunId,'\ - 'capacityGb,'\ - 'bytesUsed,'\ - 'serviceResource.datacenter[name],'\ - 'serviceResourceBackendIpAddress,'\ - 'activeTransactionCount,'\ + expected_mask = 'id,' \ + 'username,' \ + 'lunId,' \ + 'capacityGb,' \ + 'bytesUsed,' \ + 'serviceResource.datacenter[name],' \ + 'serviceResourceBackendIpAddress,' \ + 'activeTransactionCount,' \ 'replicationPartnerCount' self.assert_called_with( @@ -138,14 +177,14 @@ } } - expected_mask = 'id,'\ - 'username,'\ - 'lunId,'\ - 'capacityGb,'\ - 'bytesUsed,'\ - 'serviceResource.datacenter[name],'\ - 'serviceResourceBackendIpAddress,'\ - 'activeTransactionCount,'\ + expected_mask = 'id,' \ + 'username,' \ + 'lunId,' \ + 'capacityGb,' \ + 'bytesUsed,' \ + 'serviceResource.datacenter[name],' \ + 'serviceResourceBackendIpAddress,' \ + 'activeTransactionCount,' \ 'replicationPartnerCount' self.assert_called_with( @@ -358,7 +397,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, @@ -402,7 +441,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, @@ -526,7 +565,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_Network_' - 'Storage_Enterprise_SnapshotSpace_Upgrade', + 'Storage_Enterprise_SnapshotSpace_Upgrade', 'packageId': 759, 'prices': [ {'id': 193853} @@ -555,7 +594,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_Network_' - 'Storage_Enterprise_SnapshotSpace', + 'Storage_Enterprise_SnapshotSpace', 'packageId': 759, 'prices': [ {'id': 193613} @@ -611,7 +650,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, @@ -652,7 +691,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, @@ -710,7 +749,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, @@ -743,7 +782,7 @@ duplicate_iops=2000, duplicate_tier_level=None, duplicate_snapshot_size=10 - ) + ) self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) @@ -752,7 +791,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, @@ -790,7 +829,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, @@ -821,7 +860,7 @@ duplicate_iops=None, duplicate_tier_level=4, duplicate_snapshot_size=10 - ) + ) self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) @@ -830,7 +869,7 @@ 'placeOrder', args=({ 'complexType': 'SoftLayer_Container_Product_Order_' - 'Network_Storage_AsAService', + 'Network_Storage_AsAService', 'packageId': 759, 'prices': [ {'id': 189433}, diff -Nru python-softlayer-5.4.4/tests/managers/dedicated_host_tests.py python-softlayer-5.6.4/tests/managers/dedicated_host_tests.py --- python-softlayer-5.4.4/tests/managers/dedicated_host_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/dedicated_host_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -90,7 +90,7 @@ { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': u'test.com', @@ -103,7 +103,7 @@ 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -133,6 +133,57 @@ 'placeOrder', args=(values,)) + def test_place_order_with_gpu(self): + create_dict = self.dedicated_host._generate_create_dict = mock.Mock() + + values = { + 'hardware': [ + { + 'primaryBackendNetworkComponent': { + 'router': { + 'id': 12345 + } + }, + 'domain': u'test.com', + 'hostname': u'test' + } + ], + 'useHourlyPricing': True, + 'location': 'AMSTERDAM', + 'packageId': 813, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'prices': [ + { + 'id': 12345 + } + ], + 'quantity': 1 + } + create_dict.return_value = values + + location = 'dal05' + hostname = 'test' + domain = 'test.com' + hourly = True + flavor = '56_CORES_X_484_RAM_X_1_5_TB_X_2_GPU_P100' + + self.dedicated_host.place_order(hostname=hostname, + domain=domain, + location=location, + flavor=flavor, + hourly=hourly) + + create_dict.assert_called_once_with(hostname=hostname, + router=None, + domain=domain, + datacenter=location, + flavor=flavor, + hourly=True) + + self.assert_called_with('SoftLayer_Product_Order', + 'placeOrder', + args=(values,)) + def test_verify_order(self): create_dict = self.dedicated_host._generate_create_dict = mock.Mock() @@ -141,7 +192,7 @@ { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -154,7 +205,7 @@ 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -208,7 +259,7 @@ { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -221,7 +272,7 @@ 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -233,10 +284,10 @@ self.dedicated_host._get_package = mock.MagicMock() self.dedicated_host._get_package.return_value = self._get_package() self.dedicated_host._get_default_router = mock.Mock() - self.dedicated_host._get_default_router.return_value = 51218 + self.dedicated_host._get_default_router.return_value = 12345 location = 'dal05' - router = 51218 + router = 12345 hostname = 'test' domain = 'test.com' hourly = True @@ -255,7 +306,7 @@ { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -269,7 +320,7 @@ 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -286,7 +337,8 @@ capacity, keyName, itemCategory[categoryCode], - bundleItems[capacity, categories[categoryCode]] + bundleItems[capacity,keyName,categories[categoryCode],hardwareGenericComponentModel[id, + hardwareComponentType[keyName]]] ], regions[location[location[priceGroups]]] ''' @@ -369,7 +421,7 @@ def test_get_price(self): package = self._get_package() item = package['items'][0] - price_id = 200269 + price_id = 12345 self.assertEqual(self.dedicated_host._get_price(item), price_id) @@ -388,27 +440,29 @@ item = { 'bundleItems': [{ 'capacity': '1200', + 'keyName': '1_4_TB_LOCAL_STORAGE_DEDICATED_HOST_CAPACITY', 'categories': [{ 'categoryCode': 'dedicated_host_disk' }] }, { 'capacity': '242', + 'keyName': '242_GB_RAM', 'categories': [{ 'categoryCode': 'dedicated_host_ram' }] - }], + }], 'capacity': '56', 'description': '56 Cores X 242 RAM X 1.2 TB', - 'id': 10195, + 'id': 12345, 'itemCategory': { 'categoryCode': 'dedicated_virtual_hosts' }, 'keyName': '56_CORES_X_242_RAM_X_1_4_TB', 'prices': [{ 'hourlyRecurringFee': '3.164', - 'id': 200269, - 'itemId': 10195, + 'id': 12345, + 'itemId': 12345, 'recurringFee': '2099', }] } @@ -427,7 +481,7 @@ location = [ { 'isAvailable': 1, - 'locationId': 138124, + 'locationId': 12345, 'packageId': 813 } ] @@ -474,7 +528,7 @@ def test_get_default_router(self): routers = self._get_routers_sample() - router = 51218 + router = 12345 router_test = self.dedicated_host._get_default_router(routers, 'bcr01a.dal05') @@ -486,23 +540,64 @@ self.assertRaises(exceptions.SoftLayerError, self.dedicated_host._get_default_router, routers, 'notFound') + def test_cancel_host(self): + result = self.dedicated_host.cancel_host(789) + + self.assertEqual(result, True) + self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'deleteObject', identifier=789) + + def test_cancel_guests(self): + vs1 = {'id': 987, 'fullyQualifiedDomainName': 'foobar.example.com'} + vs2 = {'id': 654, 'fullyQualifiedDomainName': 'wombat.example.com'} + self.dedicated_host.host = mock.Mock() + self.dedicated_host.host.getGuests.return_value = [vs1, vs2] + + # Expected result + vs_status1 = {'id': 987, 'fqdn': 'foobar.example.com', 'status': 'Cancelled'} + vs_status2 = {'id': 654, 'fqdn': 'wombat.example.com', 'status': 'Cancelled'} + delete_status = [vs_status1, vs_status2] + + result = self.dedicated_host.cancel_guests(789) + + self.assertEqual(result, delete_status) + + def test_cancel_guests_empty_list(self): + self.dedicated_host.host = mock.Mock() + self.dedicated_host.host.getGuests.return_value = [] + + result = self.dedicated_host.cancel_guests(789) + + self.assertEqual(result, []) + + def test_delete_guest(self): + result = self.dedicated_host._delete_guest(123) + self.assertEqual(result, 'Cancelled') + + # delete_guest should return the exception message in case it fails + error_raised = SoftLayer.SoftLayerAPIError('SL Exception', 'SL message') + self.dedicated_host.guest = mock.Mock() + self.dedicated_host.guest.deleteObject.side_effect = error_raised + + result = self.dedicated_host._delete_guest(369) + self.assertEqual(result, 'Exception: SL message') + def _get_routers_sample(self): routers = [ { 'hostname': 'bcr01a.dal05', - 'id': 51218 + 'id': 12345 }, { 'hostname': 'bcr02a.dal05', - 'id': 83361 + 'id': 12346 }, { 'hostname': 'bcr03a.dal05', - 'id': 122762 + 'id': 12347 }, { 'hostname': 'bcr04a.dal05', - 'id': 147566 + 'id': 12348 } ] @@ -517,6 +612,7 @@ "bundleItems": [ { "capacity": "1200", + "keyName": "1_4_TB_LOCAL_STORAGE_DEDICATED_HOST_CAPACITY", "categories": [ { "categoryCode": "dedicated_host_disk" @@ -525,6 +621,7 @@ }, { "capacity": "242", + "keyName": "242_GB_RAM", "categories": [ { "categoryCode": "dedicated_host_ram" @@ -534,14 +631,14 @@ ], "prices": [ { - "itemId": 10195, + "itemId": 12345, "recurringFee": "2099", "hourlyRecurringFee": "3.164", - "id": 200269, + "id": 12345, } ], "keyName": "56_CORES_X_242_RAM_X_1_4_TB", - "id": 10195, + "id": 12345, "itemCategory": { "categoryCode": "dedicated_virtual_hosts" }, @@ -552,12 +649,12 @@ "location": { "locationPackageDetails": [ { - "locationId": 265592, + "locationId": 12345, "packageId": 813 } ], "location": { - "id": 265592, + "id": 12345, "name": "ams01", "longName": "Amsterdam 1" } @@ -571,12 +668,12 @@ "locationPackageDetails": [ { "isAvailable": 1, - "locationId": 138124, + "locationId": 12345, "packageId": 813 } ], "location": { - "id": 138124, + "id": 12345, "name": "dal05", "longName": "Dallas 5" } @@ -591,3 +688,34 @@ } return package + + def test_list_guests(self): + results = self.dedicated_host.list_guests(12345) + + for result in results: + self.assertIn(result['id'], [200, 202]) + self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'getGuests', identifier=12345) + + def test_list_guests_with_filters(self): + self.dedicated_host.list_guests(12345, tags=['tag1', 'tag2'], cpus=2, memory=1024, + hostname='hostname', domain='example.com', nic_speed=100, + public_ip='1.2.3.4', private_ip='4.3.2.1') + + _filter = { + 'guests': { + 'domain': {'operation': '_= example.com'}, + 'tagReferences': { + 'tag': {'name': { + 'operation': 'in', + 'options': [{ + 'name': 'data', 'value': ['tag1', 'tag2']}]}}}, + 'maxCpu': {'operation': 2}, + 'maxMemory': {'operation': 1024}, + 'hostname': {'operation': '_= hostname'}, + 'networkComponents': {'maxSpeed': {'operation': 100}}, + 'primaryIpAddress': {'operation': '_= 1.2.3.4'}, + 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'} + } + } + self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'getGuests', + identifier=12345, filter=_filter) diff -Nru python-softlayer-5.4.4/tests/managers/dns_tests.py python-softlayer-5.6.4/tests/managers/dns_tests.py --- python-softlayer-5.4.4/tests/managers/dns_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/dns_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -91,6 +91,73 @@ },)) self.assertEqual(res, {'name': 'example.com'}) + def test_create_record_mx(self): + res = self.dns_client.create_record_mx(1, 'test', 'testing', ttl=1200, priority=21) + + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=({ + 'domainId': 1, + 'ttl': 1200, + 'host': 'test', + 'type': 'MX', + 'data': 'testing', + 'mxPriority': 21 + },)) + self.assertEqual(res, {'name': 'example.com'}) + + def test_create_record_srv(self): + res = self.dns_client.create_record_srv(1, 'record', 'test_data', 'SLS', 8080, 'foobar', + ttl=1200, priority=21, weight=15) + + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=({ + 'complexType': 'SoftLayer_Dns_Domain_ResourceRecord_SrvType', + 'domainId': 1, + 'ttl': 1200, + 'host': 'record', + 'type': 'SRV', + 'data': 'test_data', + 'priority': 21, + 'weight': 15, + 'service': 'foobar', + 'port': 8080, + 'protocol': 'SLS' + },)) + self.assertEqual(res, {'name': 'example.com'}) + + def test_create_record_ptr(self): + res = self.dns_client.create_record_ptr('test', 'testing', ttl=1200) + + self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', + 'createObject', + args=({ + 'ttl': 1200, + 'host': 'test', + 'type': 'PTR', + 'data': 'testing' + },)) + self.assertEqual(res, {'name': 'example.com'}) + + def test_generate_create_dict(self): + data = self.dns_client._generate_create_dict('foo', 'pmx', 'bar', 60, domainId=1234, + mxPriority=18, port=80, protocol='TCP', weight=25) + + assert_data = { + 'host': 'foo', + 'data': 'bar', + 'ttl': 60, + 'type': 'pmx', + 'domainId': 1234, + 'mxPriority': 18, + 'port': 80, + 'protocol': 'TCP', + 'weight': 25 + } + + self.assertEqual(data, assert_data) + def test_delete_record(self): self.dns_client.delete_record(1) diff -Nru python-softlayer-5.4.4/tests/managers/file_tests.py python-softlayer-5.6.4/tests/managers/file_tests.py --- python-softlayer-5.4.4/tests/managers/file_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/file_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -682,7 +682,7 @@ duplicate_iops=2000, duplicate_tier_level=None, duplicate_snapshot_size=10 - ) + ) self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) @@ -760,7 +760,7 @@ duplicate_iops=None, duplicate_tier_level=4, duplicate_snapshot_size=10 - ) + ) self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) diff -Nru python-softlayer-5.4.4/tests/managers/hardware_tests.py python-softlayer-5.6.4/tests/managers/hardware_tests.py --- python-softlayer-5.4.4/tests/managers/hardware_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/hardware_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -37,6 +37,7 @@ self.assertEqual(mgr.ordering_manager, ordering_manager) def test_list_hardware(self): + # Cast result back to list because list_hardware is now a generator results = self.hardware.list_hardware() self.assertEqual(results, fixtures.SoftLayer_Account.getHardware) @@ -287,7 +288,7 @@ ex = self.assertRaises(SoftLayer.SoftLayerError, self.hardware.cancel_hardware, 6327) - self.assertEqual("Ticket #1234 already exists for this server", str(ex)) + self.assertEqual("Ticket #1234 already exists for this server", str(ex)) def test_cancel_hardware_monthly_now(self): mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') diff -Nru python-softlayer-5.4.4/tests/managers/image_tests.py python-softlayer-5.6.4/tests/managers/image_tests.py --- python-softlayer-5.4.4/tests/managers/image_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/image_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -145,6 +145,36 @@ 'uri': 'someuri', 'operatingSystemReferenceCode': 'UBUNTU_LATEST'},)) + def test_import_image_cos(self): + self.image.import_image_from_uri(name='test_image', + note='testimage', + uri='cos://some_uri', + os_code='UBUNTU_LATEST', + ibm_api_key='some_ibm_key', + root_key_id='some_root_key_id', + wrapped_dek='some_dek', + kp_id='some_id', + cloud_init=False, + byol=False, + is_encrypted=False + ) + + self.assert_called_with( + IMAGE_SERVICE, + 'createFromIcos', + args=({'name': 'test_image', + 'note': 'testimage', + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'uri': 'cos://some_uri', + 'ibmApiKey': 'some_ibm_key', + 'rootKeyId': 'some_root_key_id', + 'wrappedDek': 'some_dek', + 'keyProtectId': 'some_id', + 'cloudInit': False, + 'byol': False, + 'isEncrypted': False + },)) + def test_export_image(self): self.image.export_image_to_uri(1234, 'someuri') @@ -153,3 +183,14 @@ 'copyToExternalSource', args=({'uri': 'someuri'},), identifier=1234) + + def test_export_image_cos(self): + self.image.export_image_to_uri(1234, + 'cos://someuri', + ibm_api_key='someApiKey') + + self.assert_called_with( + IMAGE_SERVICE, + 'copyToIcos', + args=({'uri': 'cos://someuri', 'ibmApiKey': 'someApiKey'},), + identifier=1234) diff -Nru python-softlayer-5.4.4/tests/managers/ipsec_tests.py python-softlayer-5.6.4/tests/managers/ipsec_tests.py --- python-softlayer-5.4.4/tests/managers/ipsec_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/ipsec_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -125,7 +125,7 @@ def test_get_translation(self): mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') mock.return_value = [{'id': 445, 'addressTranslations': - [{'id': 234123}, {'id': 872341}]}] + [{'id': 234123}, {'id': 872341}]}] self.assertEqual(self.ipsec.get_translation(445, 872341), {'id': 872341, 'customerIpAddress': '', @@ -136,7 +136,7 @@ def test_get_translation_raises_error(self): mock = self.set_mock('SoftLayer_Account', 'getNetworkTunnelContexts') mock.return_value = [{'id': 445, 'addressTranslations': - [{'id': 234123}]}] + [{'id': 234123}]}] self.assertRaises(SoftLayerAPIError, self.ipsec.get_translation, 445, diff -Nru python-softlayer-5.4.4/tests/managers/ordering_tests.py python-softlayer-5.6.4/tests/managers/ordering_tests.py --- python-softlayer-5.4.4/tests/managers/ordering_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/ordering_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -68,6 +68,18 @@ self.assertEqual(46, package_id) + def test_get_preset_prices(self): + result = self.ordering.get_preset_prices(405) + + self.assertEqual(result, fixtures.SoftLayer_Product_Package_Preset.getObject) + self.assert_called_with('SoftLayer_Product_Package_Preset', 'getObject') + + def test_get_item_prices(self): + result = self.ordering.get_item_prices(835) + + self.assertEqual(result, fixtures.SoftLayer_Product_Package.getItemPrices) + self.assert_called_with('SoftLayer_Product_Package', 'getItemPrices') + def test_get_package_id_by_type_fails_for_nonexistent_package_type(self): p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') p_mock.return_value = [] @@ -283,32 +295,50 @@ self.assertEqual('Preset {} does not exist in package {}'.format(keyname, 'PACKAGE_KEYNAME'), str(exc)) def test_get_price_id_list(self): - price1 = {'id': 1234, 'locationGroupId': None} - item1 = {'id': 1111, 'keyName': 'ITEM1', 'prices': [price1]} - price2 = {'id': 5678, 'locationGroupId': None} - item2 = {'id': 2222, 'keyName': 'ITEM2', 'prices': [price2]} + category1 = {'categoryCode': 'cat1'} + price1 = {'id': 1234, 'locationGroupId': None, 'categories': [{"categoryCode": "guest_core"}], + 'itemCategory': [category1]} + item1 = {'id': 1111, 'keyName': 'ITEM1', 'itemCategory': category1, 'prices': [price1]} + category2 = {'categoryCode': 'cat2'} + price2 = {'id': 5678, 'locationGroupId': None, 'categories': [category2]} + item2 = {'id': 2222, 'keyName': 'ITEM2', 'itemCategory': category2, 'prices': [price2]} with mock.patch.object(self.ordering, 'list_items') as list_mock: list_mock.return_value = [item1, item2] - prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2']) + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2'], "8") - list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, keyName, prices') + list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) def test_get_price_id_list_item_not_found(self): - price1 = {'id': 1234, 'locationGroupId': ''} - item1 = {'id': 1111, 'keyName': 'ITEM1', 'prices': [price1]} + category1 = {'categoryCode': 'cat1'} + price1 = {'id': 1234, 'locationGroupId': '', 'categories': [category1]} + item1 = {'id': 1111, 'keyName': 'ITEM1', 'itemCategory': category1, 'prices': [price1]} with mock.patch.object(self.ordering, 'list_items') as list_mock: list_mock.return_value = [item1] exc = self.assertRaises(exceptions.SoftLayerError, self.ordering.get_price_id_list, - 'PACKAGE_KEYNAME', ['ITEM2']) - list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, keyName, prices') + 'PACKAGE_KEYNAME', ['ITEM2'], "8") + list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual("Item ITEM2 does not exist for package PACKAGE_KEYNAME", str(exc)) + def test_get_price_id_list_gpu_items_with_two_categories(self): + # Specific for GPU prices which are differentiated by their category (gpu0, gpu1) + price1 = {'id': 1234, 'locationGroupId': None, 'categories': [{'categoryCode': 'gpu1'}]} + price2 = {'id': 5678, 'locationGroupId': None, 'categories': [{'categoryCode': 'gpu0'}]} + item1 = {'id': 1111, 'keyName': 'ITEM1', 'itemCategory': {'categoryCode': 'gpu0'}, 'prices': [price1, price2]} + + with mock.patch.object(self.ordering, 'list_items') as list_mock: + list_mock.return_value = [item1, item1] + + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM1'], "8") + + list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') + self.assertEqual([price2['id'], price1['id']], prices) + def test_generate_no_complex_type(self): pkg = 'PACKAGE_KEYNAME' items = ['ITEM1', 'ITEM2'] @@ -321,13 +351,15 @@ complex_type = 'SoftLayer_Container_Foo' items = ['ITEM1', 'ITEM2'] preset = 'PRESET_KEYNAME' - expected_order = {'complexType': 'SoftLayer_Container_Foo', - 'location': 1854895, - 'packageId': 1234, - 'presetId': 5678, - 'prices': [{'id': 1111}, {'id': 2222}], - 'quantity': 1, - 'useHourlyPricing': True} + expected_order = {'orderContainers': [ + {'complexType': 'SoftLayer_Container_Foo', + 'location': 1854895, + 'packageId': 1234, + 'presetId': 5678, + 'prices': [{'id': 1111}, {'id': 2222}], + 'quantity': 1, + 'useHourlyPricing': True} + ]} mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() @@ -335,19 +367,21 @@ mock_pkg.assert_called_once_with(pkg, mask='id') mock_preset.assert_called_once_with(pkg, preset) - mock_get_ids.assert_called_once_with(pkg, items) + mock_get_ids.assert_called_once_with(pkg, items, 8) self.assertEqual(expected_order, order) def test_generate_order(self): pkg = 'PACKAGE_KEYNAME' items = ['ITEM1', 'ITEM2'] complex_type = 'My_Type' - expected_order = {'complexType': 'My_Type', - 'location': 1854895, - 'packageId': 1234, - 'prices': [{'id': 1111}, {'id': 2222}], - 'quantity': 1, - 'useHourlyPricing': True} + expected_order = {'orderContainers': [ + {'complexType': 'My_Type', + 'location': 1854895, + 'packageId': 1234, + 'prices': [{'id': 1111}, {'id': 2222}], + 'quantity': 1, + 'useHourlyPricing': True} + ]} mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() @@ -355,7 +389,7 @@ mock_pkg.assert_called_once_with(pkg, mask='id') mock_preset.assert_not_called() - mock_get_ids.assert_called_once_with(pkg, items) + mock_get_ids.assert_called_once_with(pkg, items, None) self.assertEqual(expected_order, order) def test_verify_order(self): @@ -410,6 +444,33 @@ extras=extras, quantity=quantity) self.assertEqual(ord_mock.return_value, order) + def test_place_quote(self): + ord_mock = self.set_mock('SoftLayer_Product_Order', 'placeQuote') + ord_mock.return_value = {'id': 1234} + pkg = 'PACKAGE_KEYNAME' + location = 'DALLAS13' + items = ['ITEM1', 'ITEM2'] + hourly = False + preset_keyname = 'PRESET' + complex_type = 'Complex_Type' + extras = {'foo': 'bar'} + quantity = 1 + name = 'wombat' + send_email = True + + with mock.patch.object(self.ordering, 'generate_order') as gen_mock: + gen_mock.return_value = {'order': {}} + + order = self.ordering.place_quote(pkg, location, items, preset_keyname=preset_keyname, + complex_type=complex_type, extras=extras, quantity=quantity, + quote_name=name, send_email=send_email) + + gen_mock.assert_called_once_with(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + complex_type=complex_type, + extras=extras, quantity=quantity) + self.assertEqual(ord_mock.return_value, order) + def test_locations(self): locations = self.ordering.package_locations('BARE_METAL_CPU') self.assertEqual('WASHINGTON07', locations[0]['keyname']) @@ -448,7 +509,7 @@ def test_get_location_id_exception(self): locations = self.set_mock('SoftLayer_Location', 'getDatacenters') locations.return_value = [] - self.assertRaises(exceptions.SoftLayerError, self.ordering.get_location_id, "BURMUDA") + self.assertRaises(exceptions.SoftLayerError, self.ordering.get_location_id, "BURMUDA") def test_get_location_id_int(self): dc_id = self.ordering.get_location_id(1234) @@ -456,30 +517,83 @@ def test_location_group_id_none(self): # RestTransport uses None for empty locationGroupId - price1 = {'id': 1234, 'locationGroupId': None} - item1 = {'id': 1111, 'keyName': 'ITEM1', 'prices': [price1]} - price2 = {'id': 5678, 'locationGroupId': None} - item2 = {'id': 2222, 'keyName': 'ITEM2', 'prices': [price2]} + category1 = {'categoryCode': 'cat1'} + price1 = {'id': 1234, 'locationGroupId': None, 'categories': [category1]} + item1 = {'id': 1111, 'keyName': 'ITEM1', 'itemCategory': category1, 'prices': [price1]} + category2 = {'categoryCode': 'cat2'} + price2 = {'id': 5678, 'locationGroupId': None, 'categories': [category2]} + item2 = {'id': 2222, 'keyName': 'ITEM2', 'itemCategory': category2, 'prices': [price2]} with mock.patch.object(self.ordering, 'list_items') as list_mock: list_mock.return_value = [item1, item2] - prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2']) + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2'], "8") - list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, keyName, prices') + list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) def test_location_groud_id_empty(self): # XMLRPCtransport uses '' for empty locationGroupId - price1 = {'id': 1234, 'locationGroupId': ''} - item1 = {'id': 1111, 'keyName': 'ITEM1', 'prices': [price1]} - price2 = {'id': 5678, 'locationGroupId': ""} - item2 = {'id': 2222, 'keyName': 'ITEM2', 'prices': [price2]} + category1 = {'categoryCode': 'cat1'} + price1 = {'id': 1234, 'locationGroupId': '', 'categories': [category1]} + item1 = {'id': 1111, 'keyName': 'ITEM1', 'itemCategory': category1, 'prices': [price1]} + category2 = {'categoryCode': 'cat2'} + price2 = {'id': 5678, 'locationGroupId': "", 'categories': [category2]} + item2 = {'id': 2222, 'keyName': 'ITEM2', 'itemCategory': category2, 'prices': [price2]} with mock.patch.object(self.ordering, 'list_items') as list_mock: list_mock.return_value = [item1, item2] - prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2']) + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2'], "8") - list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, keyName, prices') + list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) + + def test_get_item_price_id_without_capacity_restriction(self): + category1 = {'categoryCode': 'cat1'} + category2 = {'categoryCode': 'cat2'} + prices = [{'id': 1234, 'locationGroupId': '', 'categories': [category1]}, + {'id': 2222, 'locationGroupId': 509, 'categories': [category2]}] + + price_id = self.ordering.get_item_price_id("8", prices) + + self.assertEqual(1234, price_id) + + def test_get_item_price_id_with_capacity_restriction(self): + category1 = {'categoryCode': 'cat1'} + price1 = [{'id': 1234, 'locationGroupId': '', "capacityRestrictionMaximum": "16", + "capacityRestrictionMinimum": "1", 'categories': [category1]}, + {'id': 2222, 'locationGroupId': '', "capacityRestrictionMaximum": "56", + "capacityRestrictionMinimum": "36", 'categories': [category1]}] + + price_id = self.ordering.get_item_price_id("8", price1) + + self.assertEqual(1234, price_id) + + def test_issues1067(self): + # https://github.com/softlayer/softlayer-python/issues/1067 + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock_return = [ + { + 'id': 10453, + 'itemCategory': {'categoryCode': 'server'}, + 'keyName': 'INTEL_INTEL_XEON_4110_2_10', + 'prices': [ + { + 'capacityRestrictionMaximum': '2', + 'capacityRestrictionMinimum': '2', + 'capacityRestrictionType': 'PROCESSOR', + 'categories': [{'categoryCode': 'os'}], + 'id': 201161, + 'locationGroupId': None, + 'recurringFee': '250', + 'setupFee': '0' + } + ] + } + ] + item_mock.return_value = item_mock_return + item_keynames = ['INTEL_INTEL_XEON_4110_2_10'] + package = 'DUAL_INTEL_XEON_PROCESSOR_SCALABLE_FAMILY_4_DRIVES' + result = self.ordering.get_price_id_list(package, item_keynames, None) + self.assertIn(201161, result) diff -Nru python-softlayer-5.4.4/tests/managers/sshkey_tests.py python-softlayer-5.6.4/tests/managers/sshkey_tests.py --- python-softlayer-5.4.4/tests/managers/sshkey_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/sshkey_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -19,7 +19,7 @@ notes='My notes') args = ({ - 'key': 'pretend this is a public SSH key', + 'key': 'pretend this is a public SSH key', 'label': 'Test label', 'notes': 'My notes', },) diff -Nru python-softlayer-5.4.4/tests/managers/ticket_tests.py python-softlayer-5.6.4/tests/managers/ticket_tests.py --- python-softlayer-5.4.4/tests/managers/ticket_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/ticket_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -43,6 +43,14 @@ self.assertIn(result['id'], [100, 101]) self.assert_called_with('SoftLayer_Account', 'getClosedTickets') + def test_list_tickets_false(self): + exception = self.assertRaises(ValueError, + self.ticket.list_tickets, + open_status=False, + closed_status=False) + + self.assertEqual('open_status and closed_status cannot both be False', str(exception)) + def test_list_subjects(self): list_expected_ids = [1001, 1002, 1003, 1004, 1005] diff -Nru python-softlayer-5.4.4/tests/managers/user_tests.py python-softlayer-5.6.4/tests/managers/user_tests.py --- python-softlayer-5.4.4/tests/managers/user_tests.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/user_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,193 @@ +""" + SoftLayer.tests.managers.user_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +""" +import datetime +import mock +import SoftLayer +from SoftLayer import exceptions +from SoftLayer import testing + + +real_datetime_class = datetime.datetime + + +def mock_datetime(target, datetime_module): + """A way to use specific datetimes in tests. Just mocking datetime doesn't work because of pypy + + https://solidgeargroup.com/mocking-the-time + """ + class DatetimeSubclassMeta(type): + @classmethod + def __instancecheck__(mcs, obj): + return isinstance(obj, real_datetime_class) + + class BaseMockedDatetime(real_datetime_class): + @classmethod + def now(cls, tz=None): + return target.replace(tzinfo=tz) + + @classmethod + def utcnow(cls): + return target + + @classmethod + def today(cls): + return target + + # Python2 & Python3-compatible metaclass + MockedDatetime = DatetimeSubclassMeta('datetime', (BaseMockedDatetime,), {}) + + return mock.patch.object(datetime_module, 'datetime', MockedDatetime) + + +class UserManagerTests(testing.TestCase): + + def set_up(self): + self.manager = SoftLayer.UserManager(self.client) + + def test_list_user_defaults(self): + self.manager.list_users() + self.assert_called_with('SoftLayer_Account', 'getUsers', mask=mock.ANY) + + def test_list_user_mask(self): + self.manager.list_users(objectmask="mask[id]") + self.assert_called_with('SoftLayer_Account', 'getUsers', mask="mask[id]") + + def test_list_user_filter(self): + test_filter = {'id': {'operation': 1234}} + self.manager.list_users(objectfilter=test_filter) + self.assert_called_with('SoftLayer_Account', 'getUsers', filter=test_filter) + + def test_get_user_default(self): + self.manager.get_user(1234) + self.assert_called_with('SoftLayer_User_Customer', 'getObject', identifier=1234, + mask="mask[userStatus[name], parent[id, username]]") + + def test_get_user_mask(self): + self.manager.get_user(1234, objectmask="mask[id]") + self.assert_called_with('SoftLayer_User_Customer', 'getObject', identifier=1234, mask="mask[id]") + + def test_get_all_permissions(self): + self.manager.get_all_permissions() + self.assert_called_with('SoftLayer_User_Customer_CustomerPermission_Permission', 'getAllObjects') + + def test_add_permissions(self): + self.manager.add_permissions(1234, ['TEST']) + expected_args = ( + [{'keyName': 'TEST'}], + ) + self.assert_called_with('SoftLayer_User_Customer', 'addBulkPortalPermission', + args=expected_args, identifier=1234) + + def test_remove_permissions(self): + self.manager.remove_permissions(1234, ['TEST']) + expected_args = ( + [{'keyName': 'TEST'}], + ) + self.assert_called_with('SoftLayer_User_Customer', 'removeBulkPortalPermission', + args=expected_args, identifier=1234) + + def test_get_logins_default(self): + target = datetime.datetime(2018, 5, 15) + with mock_datetime(target, datetime): + self.manager.get_logins(1234) + expected_filter = { + 'loginAttempts': { + 'createDate': { + 'operation': 'greaterThanDate', + 'options': [{'name': 'date', 'value': ['04/15/2018 0:0:0']}] + } + } + } + self.assert_called_with('SoftLayer_User_Customer', 'getLoginAttempts', filter=expected_filter) + + def test_get_events_default(self): + target = datetime.datetime(2018, 5, 15) + with mock_datetime(target, datetime): + + self.manager.get_events(1234) + expected_filter = { + 'userId': { + 'operation': 1234 + }, + 'eventCreateDate': { + 'operation': 'greaterThanDate', + 'options': [{'name': 'date', 'value': ['2018-04-15T00:00:00']}] + } + } + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=expected_filter) + + def test_get_events_empty(self): + event_mock = self.set_mock('SoftLayer_Event_Log', 'getAllObjects') + event_mock.return_value = None + result = self.manager.get_events(1234) + + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=mock.ANY) + self.assertEqual([{'eventName': 'No Events Found'}], result) + + @mock.patch('SoftLayer.managers.user.UserManager.get_user_permissions') + def test_permissions_from_user(self, user_permissions): + user_permissions.return_value = [ + {"keyName": "TICKET_VIEW"}, + {"keyName": "TEST"} + ] + removed_permissions = [ + {'keyName': 'ACCESS_ALL_HARDWARE'}, + {'keyName': 'ACCESS_ALL_HARDWARE'}, + {'keyName': 'ACCOUNT_SUMMARY_VIEW'}, + {'keyName': 'ADD_SERVICE_STORAGE'}, + {'keyName': 'TEST_3'}, + {'keyName': 'TEST_4'} + ] + self.manager.permissions_from_user(1234, 5678) + self.assert_called_with('SoftLayer_User_Customer', 'addBulkPortalPermission', + args=(user_permissions.return_value,)) + self.assert_called_with('SoftLayer_User_Customer', 'removeBulkPortalPermission', + args=(removed_permissions,)) + + def test_get_id_from_username_one_match(self): + account_mock = self.set_mock('SoftLayer_Account', 'getUsers') + account_mock.return_value = [{'id': 1234}] + user_id = self.manager._get_id_from_username('testUser') + expected_filter = {'users': {'username': {'operation': '_= testUser'}}} + self.assert_called_with('SoftLayer_Account', 'getUsers', filter=expected_filter, mask="mask[id, username]") + self.assertEqual([1234], user_id) + + def test_get_id_from_username_multiple_match(self): + account_mock = self.set_mock('SoftLayer_Account', 'getUsers') + account_mock.return_value = [{'id': 1234}, {'id': 4567}] + self.assertRaises(exceptions.SoftLayerError, self.manager._get_id_from_username, 'testUser') + + def test_get_id_from_username_zero_match(self): + account_mock = self.set_mock('SoftLayer_Account', 'getUsers') + account_mock.return_value = [] + self.assertRaises(exceptions.SoftLayerError, self.manager._get_id_from_username, 'testUser') + + def test_format_permission_object(self): + result = self.manager.format_permission_object(['TEST']) + self.assert_called_with('SoftLayer_User_Customer_CustomerPermission_Permission', 'getAllObjects') + self.assertEqual([{'keyName': 'TEST'}], result) + + def test_format_permission_object_all(self): + expected = [ + {'key': 'T_2', 'keyName': 'TEST', 'name': 'A Testing Permission'}, + {'key': 'T_1', 'keyName': 'TICKET_VIEW', 'name': 'View Tickets'} + ] + service_name = 'SoftLayer_User_Customer_CustomerPermission_Permission' + permission_mock = self.set_mock(service_name, 'getAllObjects') + permission_mock.return_value = expected + result = self.manager.format_permission_object(['ALL']) + self.assert_called_with(service_name, 'getAllObjects') + self.assertEqual(expected, result) + + def test_get_current_user(self): + result = self.manager.get_current_user() + self.assert_called_with('SoftLayer_Account', 'getCurrentUser', mask=mock.ANY) + self.assertEqual(result['id'], 12345) + + def test_get_current_user_mask(self): + result = self.manager.get_current_user(objectmask="mask[id]") + self.assert_called_with('SoftLayer_Account', 'getCurrentUser', mask="mask[id]") + self.assertEqual(result['id'], 12345) diff -Nru python-softlayer-5.4.4/tests/managers/vs_capacity_tests.py python-softlayer-5.6.4/tests/managers/vs_capacity_tests.py --- python-softlayer-5.4.4/tests/managers/vs_capacity_tests.py 1970-01-01 00:00:00.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/vs_capacity_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -0,0 +1,185 @@ +""" + SoftLayer.tests.managers.vs_capacity_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. + +""" +import mock + +import SoftLayer +from SoftLayer import fixtures +from SoftLayer.fixtures import SoftLayer_Product_Package +from SoftLayer import testing + + +class VSCapacityTests(testing.TestCase): + + def set_up(self): + self.manager = SoftLayer.CapacityManager(self.client) + amock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + amock.return_value = fixtures.SoftLayer_Product_Package.RESERVED_CAPACITY + + def test_list(self): + self.manager.list() + self.assert_called_with('SoftLayer_Account', 'getReservedCapacityGroups') + + def test_get_object(self): + self.manager.get_object(100) + self.assert_called_with('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject', identifier=100) + + def test_get_object_mask(self): + mask = "mask[id]" + self.manager.get_object(100, mask=mask) + self.assert_called_with('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject', identifier=100, mask=mask) + + def test_get_create_options(self): + self.manager.get_create_options() + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059, mask=mock.ANY) + + def test_get_available_routers(self): + + result = self.manager.get_available_routers() + package_filter = {'keyName': {'operation': 'RESERVED_CAPACITY'}} + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects', mask=mock.ANY, filter=package_filter) + self.assert_called_with('SoftLayer_Product_Package', 'getRegions', mask=mock.ANY) + self.assert_called_with('SoftLayer_Network_Pod', 'getAllObjects') + self.assertEqual(result[0]['keyname'], 'WASHINGTON07') + + def test_create(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + self.manager.create( + name='TEST', backend_router_id=1, flavor='B1_1X2_1_YEAR_TERM', instances=5) + + expected_args = { + 'orderContainers': [ + { + 'backendRouterId': 1, + 'name': 'TEST', + 'packageId': 1059, + 'location': 0, + 'quantity': 5, + 'useHourlyPricing': True, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', + 'prices': [{'id': 217561} + ] + } + ] + } + + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder', args=(expected_args,)) + + def test_create_test(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + self.manager.create( + name='TEST', backend_router_id=1, flavor='B1_1X2_1_YEAR_TERM', instances=5, test=True) + + expected_args = { + 'orderContainers': [ + { + 'backendRouterId': 1, + 'name': 'TEST', + 'packageId': 1059, + 'location': 0, + 'quantity': 5, + 'useHourlyPricing': True, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', + 'prices': [{'id': 217561}], + + } + ] + } + + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', args=(expected_args,)) + + def test_create_guest(self): + amock = self.set_mock('SoftLayer_Product_Package', 'getItems') + amock.return_value = fixtures.SoftLayer_Product_Package.getItems_1_IPV6_ADDRESS + guest_object = { + 'boot_mode': None, + 'disks': (), + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'hourly': True, + 'ipv6': True, + 'local_disk': None, + 'os_code': 'UBUNTU_LATEST_64', + 'primary_disk': '25', + 'private': False, + 'private_subnet': None, + 'public_subnet': None, + 'ssh_keys': [1234] + } + self.manager.create_guest(123, False, guest_object) + expectedGenerate = { + 'startCpus': None, + 'maxMemory': None, + 'hostname': 'A1538172419', + 'domain': 'test.com', + 'localDiskFlag': None, + 'hourlyBillingFlag': True, + 'supplementalCreateObjectOptions': { + 'bootMode': None, + 'flavorKeyName': 'B1_1X2X25' + }, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST_64', + 'datacenter': {'name': 'dal13'}, + 'sshKeys': [{'id': 1234}], + 'localDiskFlag': False + } + + self.assert_called_with('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject', mask=mock.ANY) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=(expectedGenerate,)) + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + # id=1059 comes from fixtures.SoftLayer_Product_Order.RESERVED_CAPACITY, production is 859 + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + def test_create_guest_no_flavor(self): + guest_object = { + 'boot_mode': None, + 'disks': (), + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'hourly': True, + 'ipv6': True, + 'local_disk': None, + 'os_code': 'UBUNTU_LATEST_64', + 'private': False, + 'private_subnet': None, + 'public_subnet': None, + 'ssh_keys': [1234] + } + self.assertRaises(SoftLayer.SoftLayerError, self.manager.create_guest, 123, False, guest_object) + + def test_create_guest_testing(self): + amock = self.set_mock('SoftLayer_Product_Package', 'getItems') + amock.return_value = fixtures.SoftLayer_Product_Package.getItems_1_IPV6_ADDRESS + guest_object = { + 'boot_mode': None, + 'disks': (), + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'hourly': True, + 'ipv6': True, + 'local_disk': None, + 'os_code': 'UBUNTU_LATEST_64', + 'primary_disk': '25', + 'private': False, + 'private_subnet': None, + 'public_subnet': None, + 'ssh_keys': [1234] + } + self.manager.create_guest(123, True, guest_object) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + + def test_flavor_string(self): + from SoftLayer.managers.vs_capacity import _flavor_string as _flavor_string + result = _flavor_string('B1_1X2_1_YEAR_TERM', '25') + self.assertEqual('B1_1X2X25', result) diff -Nru python-softlayer-5.4.4/tests/managers/vs_tests.py python-softlayer-5.6.4/tests/managers/vs_tests.py --- python-softlayer-5.4.4/tests/managers/vs_tests.py 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tests/managers/vs_tests.py 2018-11-16 23:06:56.000000000 +0000 @@ -355,6 +355,116 @@ self.assertEqual(data, assert_data) + def test_generate_public_vlan_with_public_subnet(self): + data = self.vs._generate_create_dict( + cpus=1, + memory=1, + hostname='test', + domain='example.com', + os_code="STRING", + public_vlan=1, + public_subnet=1 + ) + + assert_data = { + 'startCpus': 1, + 'maxMemory': 1, + 'hostname': 'test', + 'domain': 'example.com', + 'localDiskFlag': True, + 'operatingSystemReferenceCode': "STRING", + 'hourlyBillingFlag': True, + 'primaryNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + 'supplementalCreateObjectOptions': {'bootMode': None}, + } + + self.assertEqual(data, assert_data) + + def test_generate_private_vlan_with_private_subnet(self): + data = self.vs._generate_create_dict( + cpus=1, + memory=1, + hostname='test', + domain='example.com', + os_code="STRING", + private_vlan=1, + private_subnet=1 + ) + + assert_data = { + 'startCpus': 1, + 'maxMemory': 1, + 'hostname': 'test', + 'domain': 'example.com', + 'localDiskFlag': True, + 'operatingSystemReferenceCode': "STRING", + 'hourlyBillingFlag': True, + 'primaryBackendNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + 'supplementalCreateObjectOptions': {'bootMode': None}, + } + + self.assertEqual(data, assert_data) + + def test_generate_private_vlan_subnet_public_vlan_subnet(self): + data = self.vs._generate_create_dict( + cpus=1, + memory=1, + hostname='test', + domain='example.com', + os_code="STRING", + private_vlan=1, + private_subnet=1, + public_vlan=1, + public_subnet=1, + ) + + assert_data = { + 'startCpus': 1, + 'maxMemory': 1, + 'hostname': 'test', + 'domain': 'example.com', + 'localDiskFlag': True, + 'operatingSystemReferenceCode': "STRING", + 'hourlyBillingFlag': True, + 'primaryBackendNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + 'primaryNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + 'supplementalCreateObjectOptions': {'bootMode': None}, + } + + self.assertEqual(data, assert_data) + + def test_generate_private_subnet(self): + actual = self.assertRaises( + exceptions.SoftLayerError, + self.vs._generate_create_dict, + cpus=1, + memory=1, + hostname='test', + domain='example.com', + os_code="STRING", + private_subnet=1, + ) + + self.assertEqual(str(actual), "You need to specify a private_vlan with private_subnet") + + def test_generate_public_subnet(self): + actual = self.assertRaises( + exceptions.SoftLayerError, + self.vs._generate_create_dict, + cpus=1, + memory=1, + hostname='test', + domain='example.com', + os_code="STRING", + public_subnet=1, + ) + + self.assertEqual(str(actual), "You need to specify a public_vlan with public_subnet") + def test_generate_private_vlan(self): data = self.vs._generate_create_dict( cpus=1, @@ -373,12 +483,73 @@ 'localDiskFlag': True, 'operatingSystemReferenceCode': "STRING", 'hourlyBillingFlag': True, - 'primaryBackendNetworkComponent': {"networkVlan": {"id": 1}}, + 'primaryBackendNetworkComponent': {'networkVlan': {'id': 1}}, 'supplementalCreateObjectOptions': {'bootMode': None}, } self.assertEqual(data, assert_data) + def test_create_network_components_vlan_subnet_private_vlan_subnet_public(self): + data = self.vs._create_network_components( + private_vlan=1, + private_subnet=1, + public_vlan=1, + public_subnet=1, + ) + + assert_data = { + 'primaryBackendNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + 'primaryNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + } + + self.assertEqual(data, assert_data) + + def test_create_network_components_vlan_subnet_private(self): + data = self.vs._create_network_components( + private_vlan=1, + private_subnet=1, + ) + + assert_data = { + 'primaryBackendNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + } + + self.assertEqual(data, assert_data) + + def test_create_network_components_vlan_subnet_public(self): + data = self.vs._create_network_components( + public_vlan=1, + public_subnet=1, + ) + + assert_data = { + 'primaryNetworkComponent': {'networkVlan': {'id': 1, + 'primarySubnet': {'id': 1}}}, + } + + self.assertEqual(data, assert_data) + + def test_create_network_components_private_subnet(self): + actual = self.assertRaises( + exceptions.SoftLayerError, + self.vs._create_network_components, + private_subnet=1, + ) + + self.assertEqual(str(actual), "You need to specify a private_vlan with private_subnet") + + def test_create_network_components_public_subnet(self): + actual = self.assertRaises( + exceptions.SoftLayerError, + self.vs._create_network_components, + public_subnet=1, + ) + + self.assertEqual(str(actual), "You need to specify a public_vlan with public_subnet") + def test_generate_userdata(self): data = self.vs._generate_create_dict( cpus=1, @@ -708,6 +879,22 @@ self.assertIn({'id': 1122}, order_container['prices']) self.assertEqual(order_container['virtualGuests'], [{'id': 1}]) + def test_upgrade_with_flavor(self): + # Testing Upgrade with parameter preset + result = self.vs.upgrade(1, + preset="M1_64X512X100", + nic_speed=1000, + public=True) + + self.assertEqual(result, True) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + call = self.calls('SoftLayer_Product_Order', 'placeOrder')[0] + order_container = call.args[0] + self.assertEqual(799, order_container['presetId']) + self.assertIn({'id': 1}, order_container['virtualGuests']) + self.assertIn({'id': 1122}, order_container['prices']) + self.assertEqual(order_container['virtualGuests'], [{'id': 1}]) + def test_upgrade_dedicated_host_instance(self): mock = self.set_mock('SoftLayer_Virtual_Guest', 'getUpgradeItemPrices') mock.return_value = fixtures.SoftLayer_Virtual_Guest.DEDICATED_GET_UPGRADE_ITEM_PRICES diff -Nru python-softlayer-5.4.4/tools/requirements.txt python-softlayer-5.6.4/tools/requirements.txt --- python-softlayer-5.4.4/tools/requirements.txt 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tools/requirements.txt 2018-11-16 23:06:56.000000000 +0000 @@ -1,6 +1,7 @@ -requests >= 2.18.4 -click >= 5 -prettytable >= 0.7.0 six >= 1.7.0 -prompt_toolkit -urllib3 +ptable >= 0.9.2 +click >= 7 +requests >= 2.20.0 +prompt_toolkit >= 0.53 +pygments >= 2.0.0 +urllib3 >= 1.24 \ No newline at end of file diff -Nru python-softlayer-5.4.4/tools/test-requirements.txt python-softlayer-5.6.4/tools/test-requirements.txt --- python-softlayer-5.4.4/tools/test-requirements.txt 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tools/test-requirements.txt 2018-11-16 23:06:56.000000000 +0000 @@ -4,5 +4,10 @@ mock sphinx testtools -urllib3 -requests >= 2.18.4 +six >= 1.7.0 +ptable >= 0.9.2 +click >= 7 +requests >= 2.20.0 +prompt_toolkit >= 0.53 +pygments >= 2.0.0 +urllib3 >= 1.24 \ No newline at end of file diff -Nru python-softlayer-5.4.4/tox.ini python-softlayer-5.6.4/tox.ini --- python-softlayer-5.4.4/tox.ini 2018-04-18 18:23:37.000000000 +0000 +++ python-softlayer-5.6.4/tox.ini 2018-11-16 23:06:56.000000000 +0000 @@ -1,5 +1,6 @@ [tox] -envlist = py27,py35,py36,pypy,analysis,coverage +envlist = py27,py35,py36,py37,pypy,analysis,coverage + [flake8] max-line-length=120 @@ -35,6 +36,10 @@ -d locally-disabled \ -d no-else-return \ -d len-as-condition \ + -d useless-object-inheritance \ + -d consider-using-in \ + -d consider-using-dict-comprehension \ + -d useless-import-alias \ --max-args=25 \ --max-branches=20 \ --max-statements=65 \