diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..73e4a4e5d --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,24 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + builder: htmldir + configuration: docs/conf.py + +# Build documentation with MkDocs +#mkdocs: +# configuration: mkdocs.yml + +# Optionally build your docs in additional formats such as PDF and ePub +formats: all + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.7 + install: + - requirements: docs/requirements.txt diff --git a/.travis.yml b/.travis.yml index 735f4f693..d69b42d68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,20 @@ +# https://docs.travis-ci.com/user/languages/python/#python-37-and-higher +dist: xenial language: python sudo: false matrix: include: - - python: "2.7" - env: TOX_ENV=py27 - python: "3.5" env: TOX_ENV=py35 - python: "3.6" env: TOX_ENV=py36 - - python: "pypy2.7-5.8.0" + - python: "3.7" + env: TOX_ENV=py37 + - python: "pypy3.5" env: TOX_ENV=pypy - - python: "2.7" + - python: "3.6" env: TOX_ENV=analysis - - python: "2.7" + - python: "3.6" env: TOX_ENV=coverage install: - pip install tox diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c4d0cd7..9cf8c1149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,124 @@ # Change Log + +## [5.7.2] - 2019-05-03 +- https://github.com/softlayer/softlayer-python/compare/v5.7.1...v5.7.2 + ++ #1107 Added exception to handle json parsing error when ordering ++ #1068 Support for -1 when changing port speed ++ #1109 Fixed docs about placement groups ++ #1112 File storage endurance iops upgrade ++ #1101 Handle the new user creation exceptions ++ #1116 Fix order place quantity option ++ #1002 Invoice commands + * account invoices + * account invoice-detail + * account summary ++ #1004 Event Notification Management commands + * account events + * account event-detail ++ #1117 Two PCIe items can be added at order time ++ #1121 Fix object storage apiType for S3 and Swift. ++ #1100 Event Log performance improvements. ++ #872 column 'name' was renamed to 'hostname' ++ #1127 Fix object storage credentials. ++ #1129 Fixed unexpected errors in slcli subnet create ++ #1134 Change encrypt parameters for importing of images. Adds root-key-crn ++ #208 Quote ordering commands + * order quote + * order quote-detail + * order quote-list ++ #1113 VS usage information command + * virtual usage ++ #1131 made sure config_tests dont actually make api calls. + + +## [5.7.1] - 2019-02-26 +- https://github.com/softlayer/softlayer-python/compare/v5.7.0...v5.7.1 + ++ #1089 removed legacy SL message queue commands ++ Support for Hardware reflash firmware CLI/Manager method + +## [5.7.0] - 2019-02-15 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.6.4...v5.7.0 + ++ #1099 Support for security group Ids ++ event-log cli command ++ #1069 Virtual Placement Group Support + ``` + slcli vs placementgroup --help + Commands: + create Create a placement group. + create-options List options for creating a placement group. + delete Delete a placement group. + detail View details of a placement group. + list List placement groups. + ``` ++ #962 Rest Transport improvements. Properly handle HTTP exceptions instead of crashing. ++ #1090 removed power_state column option from "slcli server list" ++ #676 - ipv6 support for creating virtual guests + * Refactored virtual guest creation to use Product_Order::placeOrder instead of Virtual_Guest::createObject, because createObject doesn't allow adding IPv6 ++ #882 Added table which shows the status of each url in object storage ++ #1085 Update provisionedIops reading to handle float-y values ++ #1074 fixed issue with config setup ++ #1081 Fix file volume-cancel ++ #1059 Support for SoftLayer_Hardware_Server::toggleManagementInterface + * `slcli hw toggle-ipmi` + + +## [5.6.4] - 2018-11-16 + +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.6.3...v5.6.4 + ++ #1041 Dedicated host cancel, cancel-guests, list-guests ++ #1071 added createDate and modifyDate parameters to sg rule-list ++ #1060 Fixed slcli subnet list ++ #1056 Fixed documentation link in image manager ++ #1062 Added description to slcli order + +## [5.6.3] - 2018-11-07 + +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.6.0...v5.6.3 + ++ #1065 Updated urllib3 and requests libraries due to CVE-2018-18074 ++ #1070 Fixed an ordering bug ++ Updated release process and fab-file + +## [5.6.0] - 2018-10-16 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.3...v5.6.0 + ++ #1026 Support for [Reserved Capacity](https://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...master +- 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f6fd444a..1eed6d308 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,5 +25,80 @@ Code is tested and style checked with tox, you can run the tox tests individuall * create pull request +## Documentation +CLI command should have a more human readable style of documentation. +Manager methods should have a decent docblock describing any parameters and what the method does. +Docs are generated with [Sphinx](https://docs.readthedocs.io/en/latest/intro/getting-started-with-sphinx.html) and once Sphinx is setup, you can simply do + +`make html` in the softlayer-python/docs directory, which should generate the HTML in softlayer-python/docs/_build/html for testing. + + +## Unit Tests + +All new features should be 100% code covered, and your pull request should at the very least increase total code overage. + +### Mocks +To tests results from the API, we keep mock results in SoftLayer/fixtures// with the method name matching the variable name. + +Any call to a service that doesn't have a fixture will result in a TransportError + +### Overriding Fixtures + +Adding your expected output in the fixtures file with a unique name is a good way to define a fixture that gets used frequently in a test. + +```python +from SoftLayer.fixtures import SoftLayer_Product_Package + + def test_test(self): + amock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + amock.return_value = fixtures.SoftLayer_Product_Package.RESERVED_CAPACITY +``` + +Otherwise defining it on the spot works too. +```python + def test_test(self): + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = { + 'billingItem': {'hourlyFlag': True, 'id': 449}, + } +``` + + +### Call testing +Testing your code to make sure it makes the correct API call is also very important. + +The testing.TestCase class has a method call `assert_called_with` which is pretty handy here. + +```python +self.assert_called_with( + 'SoftLayer_Billing_Item', # Service + 'cancelItem', # Method + args=(True, True, ''), # Args + identifier=449, # Id + mask=mock.ANY, # object Mask, + filter=mock.ANY, # object Filter + limit=0, # result Limit + offset=0 # result Offset +) +``` + +Making sure a API was NOT called + +```python +self.assertEqual([], self.calls('SoftLayer_Account', 'getObject')) +``` + +Making sure an API call has a specific arg, but you don't want to list out the entire API call (like with a place order test) + +```python +# Get the API Call signature +order_call = self.calls('SoftLayer_Product_Order', 'placeOrder') + +# Get the args property of that API call, which is a tuple, with the first entry being our data. +order_args = getattr(order_call[0], 'args')[0] + +# Test our specific argument value +self.assertEqual(123, order_args['hostId']) +``` \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..50a35f039 --- /dev/null +++ b/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/softlayer-python.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/softlayer-python.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/softlayer-python" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/softlayer-python" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/README.rst b/README.rst index a3dc80470..1f8fb872a 100644 --- a/README.rst +++ b/README.rst @@ -12,9 +12,12 @@ SoftLayer API Python Client .. 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. @@ -28,9 +31,9 @@ http://softlayer.github.io/softlayer-python/. Additional API documentation can be found on the SoftLayer Development Network: * `SoftLayer API reference - `_ + `_ * `Object mask information and examples - `_ + `_ * `Code Examples `_ @@ -85,12 +88,14 @@ 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 @@ -115,9 +120,11 @@ If you are using the library directly in python, you can do something like this. 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. @@ -125,12 +132,12 @@ System Requirements Python Packages --------------- * six >= 1.7.0 -* prettytable >= 0.7.0 -* click >= 5 -* requests >= 2.18.4 -* prompt_toolkit >= 0.53 +* ptable >= 0.9.2 +* click >= 7 +* requests >= 2.20.0 +* prompt_toolkit >= 2 * pygments >= 2.0.0 -* urllib3 >= 1.22 +* urllib3 >= 1.24 Copyright --------- diff --git a/RELEASE.md b/RELEASE.md index 75eea45dc..eb1cb6d47 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,7 +3,13 @@ * Update version constants (find them by running `git grep [VERSION_NUMBER]`) * Create changelog entry (edit CHANGELOG.md with a one-liner for each closed issue going in the release) * Commit and push changes to master with the message: "Version Bump to v[VERSION_NUMBER]" -* Push tag and PyPi `fab release:[VERSION_NUMBER]`. Before you do this, make sure you have the organization repository set up as upstream remote & fabric installed (`pip install fabric`), also make sure that you have pip set up with your PyPi user credentials. The easiest way to do that is to create a file at `~/.pypirc` with the following contents: +* Make sure your `upstream` repo is set +``` +git remote -v +upstream git@github.com:softlayer/softlayer-python.git (fetch) +upstream git@github.com:softlayer/softlayer-python.git (push) +``` +* Push tag and PyPi `python fabfile.py 5.7.2`. Before you do this, make sure you have the organization repository set up as upstream remote, also make sure that you have pip set up with your PyPi user credentials. The easiest way to do that is to create a file at `~/.pypirc` with the following contents: ``` [server-login] diff --git a/SoftLayer/API.py b/SoftLayer/API.py index c5fd95f3a..e65da3884 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -6,8 +6,10 @@ :license: MIT, see LICENSE for more details. """ # pylint: disable=invalid-name +from __future__ import generators import warnings + from SoftLayer import auth as slauth from SoftLayer import config from SoftLayer import consts diff --git a/SoftLayer/CLI/account/__init__.py b/SoftLayer/CLI/account/__init__.py new file mode 100644 index 000000000..50da7c7f0 --- /dev/null +++ b/SoftLayer/CLI/account/__init__.py @@ -0,0 +1 @@ +"""Account commands""" diff --git a/SoftLayer/CLI/account/event_detail.py b/SoftLayer/CLI/account/event_detail.py new file mode 100644 index 000000000..2c1ee80c2 --- /dev/null +++ b/SoftLayer/CLI/account/event_detail.py @@ -0,0 +1,73 @@ +"""Details of a specific event, and ability to acknowledge event.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + + +@click.command() +@click.argument('identifier') +@click.option('--ack', is_flag=True, default=False, + help="Acknowledge Event. Doing so will turn off the popup in the control portal") +@environment.pass_env +def cli(env, identifier, ack): + """Details of a specific event, and ability to acknowledge event.""" + + # Print a list of all on going maintenance + manager = AccountManager(env.client) + event = manager.get_event(identifier) + + if ack: + manager.ack_event(identifier) + + env.fout(basic_event_table(event)) + env.fout(impacted_table(event)) + env.fout(update_table(event)) + + +def basic_event_table(event): + """Formats a basic event table""" + table = formatting.Table(["Id", "Status", "Type", "Start", "End"], + title=utils.clean_splitlines(event.get('subject'))) + + table.add_row([ + event.get('id'), + utils.lookup(event, 'statusCode', 'name'), + utils.lookup(event, 'notificationOccurrenceEventType', 'keyName'), + utils.clean_time(event.get('startDate')), + utils.clean_time(event.get('endDate')) + ]) + + return table + + +def impacted_table(event): + """Formats a basic impacted resources table""" + table = formatting.Table([ + "Type", "Id", "Hostname", "PrivateIp", "Label" + ]) + for item in event.get('impactedResources', []): + table.add_row([ + item.get('resourceType'), + item.get('resourceTableId'), + item.get('hostname'), + item.get('privateIp'), + item.get('filterLabel') + ]) + return table + + +def update_table(event): + """Formats a basic event update table""" + update_number = 0 + for update in event.get('updates', []): + header = "======= Update #%s on %s =======" % (update_number, utils.clean_time(update.get('startDate'))) + click.secho(header, fg='green') + update_number = update_number + 1 + text = update.get('contents') + # deals with all the \r\n from the API + click.secho(utils.clean_splitlines(text)) diff --git a/SoftLayer/CLI/account/events.py b/SoftLayer/CLI/account/events.py new file mode 100644 index 000000000..5cc91144d --- /dev/null +++ b/SoftLayer/CLI/account/events.py @@ -0,0 +1,54 @@ +"""Summary and acknowledgement of upcoming and ongoing maintenance events""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + + +@click.command() +@click.option('--ack-all', is_flag=True, default=False, + help="Acknowledge every upcoming event. Doing so will turn off the popup in the control portal") +@environment.pass_env +def cli(env, ack_all): + """Summary and acknowledgement of upcoming and ongoing maintenance events""" + + manager = AccountManager(env.client) + events = manager.get_upcoming_events() + + if ack_all: + for event in events: + result = manager.ack_event(event['id']) + event['acknowledgedFlag'] = result + env.fout(event_table(events)) + + +def event_table(events): + """Formats a table for events""" + table = formatting.Table([ + "Id", + "Start Date", + "End Date", + "Subject", + "Status", + "Acknowledged", + "Updates", + "Impacted Resources" + ], title="Upcoming Events") + table.align['Subject'] = 'l' + table.align['Impacted Resources'] = 'l' + for event in events: + table.add_row([ + event.get('id'), + utils.clean_time(event.get('startDate')), + utils.clean_time(event.get('endDate')), + # Some subjects can have \r\n for some reason. + utils.clean_splitlines(event.get('subject')), + utils.lookup(event, 'statusCode', 'name'), + event.get('acknowledgedFlag'), + event.get('updateCount'), + event.get('impactedResourceCount') + ]) + return table diff --git a/SoftLayer/CLI/account/invoice_detail.py b/SoftLayer/CLI/account/invoice_detail.py new file mode 100644 index 000000000..b840f3f60 --- /dev/null +++ b/SoftLayer/CLI/account/invoice_detail.py @@ -0,0 +1,61 @@ +"""Invoice details""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + + +@click.command() +@click.argument('identifier') +@click.option('--details', is_flag=True, default=False, show_default=True, + help="Shows a very detailed list of charges") +@environment.pass_env +def cli(env, identifier, details): + """Invoice details""" + + manager = AccountManager(env.client) + top_items = manager.get_billing_items(identifier) + + title = "Invoice %s" % identifier + table = formatting.Table(["Item Id", "Category", "Description", "Single", + "Monthly", "Create Date", "Location"], title=title) + table.align['category'] = 'l' + table.align['description'] = 'l' + for item in top_items: + fqdn = "%s.%s" % (item.get('hostName', ''), item.get('domainName', '')) + # category id=2046, ram_usage doesn't have a name... + category = utils.lookup(item, 'category', 'name') or item.get('categoryCode') + description = nice_string(item.get('description')) + if fqdn != '.': + description = "%s (%s)" % (item.get('description'), fqdn) + table.add_row([ + item.get('id'), + category, + nice_string(description), + "$%.2f" % float(item.get('oneTimeAfterTaxAmount')), + "$%.2f" % float(item.get('recurringAfterTaxAmount')), + utils.clean_time(item.get('createDate'), out_format="%Y-%m-%d"), + utils.lookup(item, 'location', 'name') + ]) + if details: + for child in item.get('children', []): + table.add_row([ + '>>>', + utils.lookup(child, 'category', 'name'), + nice_string(child.get('description')), + "$%.2f" % float(child.get('oneTimeAfterTaxAmount')), + "$%.2f" % float(child.get('recurringAfterTaxAmount')), + '---', + '---' + ]) + + env.fout(table) + + +def nice_string(ugly_string, limit=100): + """Format and trims strings""" + return (ugly_string[:limit] + '..') if len(ugly_string) > limit else ugly_string diff --git a/SoftLayer/CLI/account/invoices.py b/SoftLayer/CLI/account/invoices.py new file mode 100644 index 000000000..0e1b2a59f --- /dev/null +++ b/SoftLayer/CLI/account/invoices.py @@ -0,0 +1,46 @@ +"""Invoice listing""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + + +@click.command() +@click.option('--limit', default=50, show_default=True, + help="How many invoices to get back.") +@click.option('--closed', is_flag=True, default=False, show_default=True, + help="Include invoices with a CLOSED status.") +@click.option('--all', 'get_all', is_flag=True, default=False, show_default=True, + help="Return ALL invoices. There may be a lot of these.") +@environment.pass_env +def cli(env, limit, closed=False, get_all=False): + """List invoices""" + + manager = AccountManager(env.client) + invoices = manager.get_invoices(limit, closed, get_all) + + table = formatting.Table([ + "Id", "Created", "Type", "Status", "Starting Balance", "Ending Balance", "Invoice Amount", "Items" + ]) + table.align['Starting Balance'] = 'l' + table.align['Ending Balance'] = 'l' + table.align['Invoice Amount'] = 'l' + table.align['Items'] = 'l' + if isinstance(invoices, dict): + invoices = [invoices] + for invoice in invoices: + table.add_row([ + invoice.get('id'), + utils.clean_time(invoice.get('createDate'), out_format="%Y-%m-%d"), + invoice.get('typeCode'), + invoice.get('statusCode'), + invoice.get('startingBalance'), + invoice.get('endingBalance'), + invoice.get('invoiceTotalAmount'), + invoice.get('itemCount') + ]) + env.fout(table) diff --git a/SoftLayer/CLI/account/summary.py b/SoftLayer/CLI/account/summary.py new file mode 100644 index 000000000..f1ae2b6be --- /dev/null +++ b/SoftLayer/CLI/account/summary.py @@ -0,0 +1,39 @@ +"""Account Summary page""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import utils + + +@click.command() +@environment.pass_env +def cli(env): + """Prints some various bits of information about an account""" + + manager = AccountManager(env.client) + summary = manager.get_summary() + env.fout(get_snapshot_table(summary)) + + +def get_snapshot_table(account): + """Generates a table for printing account summary data""" + table = formatting.KeyValueTable(["Name", "Value"], title="Account Snapshot") + table.align['Name'] = 'r' + table.align['Value'] = 'l' + table.add_row(['Company Name', account.get('companyName', '-')]) + table.add_row(['Balance', utils.lookup(account, 'pendingInvoice', 'startingBalance')]) + table.add_row(['Upcoming Invoice', utils.lookup(account, 'pendingInvoice', 'invoiceTotalAmount')]) + table.add_row(['Image Templates', account.get('blockDeviceTemplateGroupCount', '-')]) + table.add_row(['Dedicated Hosts', account.get('dedicatedHostCount', '-')]) + table.add_row(['Hardware', account.get('hardwareCount', '-')]) + table.add_row(['Virtual Guests', account.get('virtualGuestCount', '-')]) + table.add_row(['Domains', account.get('domainCount', '-')]) + table.add_row(['Network Storage Volumes', account.get('networkStorageCount', '-')]) + table.add_row(['Open Tickets', account.get('openTicketCount', '-')]) + table.add_row(['Network Vlans', account.get('networkVlanCount', '-')]) + table.add_row(['Subnets', account.get('subnetCount', '-')]) + table.add_row(['Users', account.get('userCount', '-')]) + return table diff --git a/SoftLayer/CLI/block/access/authorize.py b/SoftLayer/CLI/block/access/authorize.py index df76b60e6..bf2da3af3 100644 --- a/SoftLayer/CLI/block/access/authorize.py +++ b/SoftLayer/CLI/block/access/authorize.py @@ -14,8 +14,7 @@ @click.option('--virtual-id', '-v', multiple=True, help='The id of one SoftLayer_Virtual_Guest to authorize') @click.option('--ip-address-id', '-i', multiple=True, - help='The id of one SoftLayer_Network_Subnet_IpAddress' - ' to authorize') + help='The id of one SoftLayer_Network_Subnet_IpAddress to authorize') @click.option('--ip-address', multiple=True, help='An IP address to authorize') @environment.pass_env @@ -30,16 +29,11 @@ def cli(env, volume_id, hardware_id, virtual_id, ip_address_id, ip_address): for ip_address_value in ip_address: ip_address_object = network_manager.ip_lookup(ip_address_value) if ip_address_object == "": - click.echo("IP Address not found on your account. " + - "Please confirm IP and try again.") + click.echo("IP Address not found on your account. Please confirm IP and try again.") raise exceptions.ArgumentError('Incorrect IP Address') - else: - ip_address_id_list.append(ip_address_object['id']) + ip_address_id_list.append(ip_address_object['id']) - block_manager.authorize_host_to_volume(volume_id, - hardware_id, - virtual_id, - ip_address_id_list) + block_manager.authorize_host_to_volume(volume_id, hardware_id, virtual_id, ip_address_id_list) # If no exception was raised, the command succeeded click.echo('The specified hosts were authorized to access %s' % volume_id) diff --git a/SoftLayer/CLI/call_api.py b/SoftLayer/CLI/call_api.py index 0adb4fa31..6e16a2a77 100644 --- a/SoftLayer/CLI/call_api.py +++ b/SoftLayer/CLI/call_api.py @@ -87,19 +87,19 @@ def cli(env, service, method, parameters, _id, _filters, mask, limit, offset, output_python=False): """Call arbitrary API endpoints with the given SERVICE and METHOD. - \b - Examples: - slcli call-api Account getObject - slcli call-api Account getVirtualGuests --limit=10 --mask=id,hostname - slcli call-api Virtual_Guest getObject --id=12345 - slcli call-api Metric_Tracking_Object getBandwidthData --id=1234 \\ - "2015-01-01 00:00:00" "2015-01-1 12:00:00" public - slcli call-api Account getVirtualGuests \\ - -f 'virtualGuests.datacenter.name=dal05' \\ - -f 'virtualGuests.maxCpu=4' \\ - --mask=id,hostname,datacenter.name,maxCpu - slcli call-api Account getVirtualGuests \\ - -f 'virtualGuests.datacenter.name IN dal05,sng01' + Example:: + + slcli call-api Account getObject + slcli call-api Account getVirtualGuests --limit=10 --mask=id,hostname + slcli call-api Virtual_Guest getObject --id=12345 + slcli call-api Metric_Tracking_Object getBandwidthData --id=1234 \\ + "2015-01-01 00:00:00" "2015-01-1 12:00:00" public + slcli call-api Account getVirtualGuests \\ + -f 'virtualGuests.datacenter.name=dal05' \\ + -f 'virtualGuests.maxCpu=4' \\ + --mask=id,hostname,datacenter.name,maxCpu + slcli call-api Account getVirtualGuests \\ + -f 'virtualGuests.datacenter.name IN dal05,sng01' """ args = [service, method] + list(parameters) diff --git a/SoftLayer/CLI/cdn/detail.py b/SoftLayer/CLI/cdn/detail.py index 509db5362..ef93d2794 100644 --- a/SoftLayer/CLI/cdn/detail.py +++ b/SoftLayer/CLI/cdn/detail.py @@ -9,24 +9,38 @@ @click.command() -@click.argument('account_id') +@click.argument('unique_id') +@click.option('--history', + default=30, type=click.IntRange(1, 89), + help='Bandwidth, Hits, Ratio counted over history number of days ago. 89 is the maximum. ') @environment.pass_env -def cli(env, account_id): +def cli(env, unique_id, history): """Detail a CDN Account.""" manager = SoftLayer.CDNManager(env.client) - account = manager.get_account(account_id) + + cdn_mapping = manager.get_cdn(unique_id) + cdn_metrics = manager.get_usage_metrics(unique_id, history=history) + + # usage metrics + total_bandwidth = "%s GB" % cdn_metrics['totals'][0] + total_hits = cdn_metrics['totals'][1] + hit_ratio = "%s %%" % cdn_metrics['totals'][2] table = formatting.KeyValueTable(['name', 'value']) table.align['name'] = 'r' table.align['value'] = 'l' - table.add_row(['id', account['id']]) - table.add_row(['account_name', account['cdnAccountName']]) - table.add_row(['type', account['cdnSolutionName']]) - table.add_row(['status', account['status']['name']]) - table.add_row(['created', account['createDate']]) - table.add_row(['notes', - account.get('cdnAccountNote', formatting.blank())]) + table.add_row(['unique_id', cdn_mapping['uniqueId']]) + table.add_row(['hostname', cdn_mapping['domain']]) + table.add_row(['protocol', cdn_mapping['protocol']]) + table.add_row(['origin', cdn_mapping['originHost']]) + table.add_row(['origin_type', cdn_mapping['originType']]) + table.add_row(['path', cdn_mapping['path']]) + table.add_row(['provider', cdn_mapping['vendorName']]) + table.add_row(['status', cdn_mapping['status']]) + table.add_row(['total_bandwidth', total_bandwidth]) + table.add_row(['total_hits', total_hits]) + table.add_row(['hit_radio', hit_ratio]) env.fout(table) diff --git a/SoftLayer/CLI/cdn/list.py b/SoftLayer/CLI/cdn/list.py index 2e1b07785..994a338b3 100644 --- a/SoftLayer/CLI/cdn/list.py +++ b/SoftLayer/CLI/cdn/list.py @@ -11,32 +11,33 @@ @click.command() @click.option('--sortby', help='Column to sort by', - type=click.Choice(['id', - 'datacenter', - 'host', - 'cores', - 'memory', - 'primary_ip', - 'backend_ip'])) + type=click.Choice(['unique_id', + 'domain', + 'origin', + 'vendor', + 'cname', + 'status'])) @environment.pass_env def cli(env, sortby): """List all CDN accounts.""" manager = SoftLayer.CDNManager(env.client) - accounts = manager.list_accounts() + accounts = manager.list_cdn() - table = formatting.Table(['id', - 'account_name', - 'type', - 'created', - 'notes']) + table = formatting.Table(['unique_id', + 'domain', + 'origin', + 'vendor', + 'cname', + 'status']) for account in accounts: table.add_row([ - account['id'], - account['cdnAccountName'], - account['cdnSolutionName'], - account['createDate'], - account.get('cdnAccountNote', formatting.blank()) + account['uniqueId'], + account['domain'], + account['originHost'], + account['vendorName'], + account['cname'], + account['status'] ]) table.sortby = sortby diff --git a/SoftLayer/CLI/cdn/load.py b/SoftLayer/CLI/cdn/load.py deleted file mode 100644 index 648f4f34e..000000000 --- a/SoftLayer/CLI/cdn/load.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Cache one or more files on all edge nodes.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - - -@click.command() -@click.argument('account_id') -@click.argument('content_url', nargs=-1) -@environment.pass_env -def cli(env, account_id, content_url): - """Cache one or more files on all edge nodes.""" - - manager = SoftLayer.CDNManager(env.client) - manager.load_content(account_id, content_url) diff --git a/SoftLayer/CLI/cdn/origin_add.py b/SoftLayer/CLI/cdn/origin_add.py index 51d789da9..08790d9b7 100644 --- a/SoftLayer/CLI/cdn/origin_add.py +++ b/SoftLayer/CLI/cdn/origin_add.py @@ -5,22 +5,82 @@ import SoftLayer from SoftLayer.CLI import environment - -# pylint: disable=redefined-builtin +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting @click.command() -@click.argument('account_id') -@click.argument('content_url') -@click.option('--type', - help='The media type for this mapping (http, flash, wm, ...)', +@click.argument('unique_id') +@click.argument('origin') +@click.argument('path') +@click.option('--origin-type', '-t', + type=click.Choice(['server', 'storage']), + help='The origin type.', + default='server', + show_default=True) +@click.option('--header', '-H', + type=click.STRING, + help='The host header to communicate with the origin.') +@click.option('--bucket-name', '-b', + type=click.STRING, + help="The name of the available resource [required if --origin-type=storage]") +@click.option('--port', '-p', + type=click.INT, + help="The http port number.", + default=80, + show_default=True) +@click.option('--protocol', '-P', + type=click.STRING, + help="The protocol used by the origin.", default='http', show_default=True) -@click.option('--cname', - help='An optional CNAME to attach to the mapping') +@click.option('--optimize-for', '-o', + type=click.Choice(['web', 'video', 'file']), + help="Performance configuration", + default='web', + show_default=True) +@click.option('--extensions', '-e', + type=click.STRING, + help="File extensions that can be stored in the CDN, example: 'jpg, png, pdf'") +@click.option('--cache-query', '-c', + type=click.STRING, + help="Cache query rules with the following formats:\n" + "'ignore-all', 'include: ', 'ignore: '", + default="include-all", + show_default=True) @environment.pass_env -def cli(env, account_id, content_url, type, cname): - """Create an origin pull mapping.""" +def cli(env, unique_id, origin, path, origin_type, header, + bucket_name, port, protocol, optimize_for, extensions, cache_query): + """Create an origin path for an existing CDN mapping. + + For more information see the following documentation: \n + https://cloud.ibm.com/docs/infrastructure/CDN?topic=CDN-manage-your-cdn#adding-origin-path-details + """ manager = SoftLayer.CDNManager(env.client) - manager.add_origin(account_id, type, content_url, cname) + + if origin_type == 'storage' and not bucket_name: + raise exceptions.ArgumentError('[-b | --bucket-name] is required when [-t | --origin-type] is "storage"') + + result = manager.add_origin(unique_id, origin, path, origin_type=origin_type, + header=header, port=port, protocol=protocol, + bucket_name=bucket_name, file_extensions=extensions, + optimize_for=optimize_for, cache_query=cache_query) + + table = formatting.Table(['Item', 'Value']) + table.align['Item'] = 'r' + table.align['Value'] = 'r' + + table.add_row(['CDN Unique ID', result['mappingUniqueId']]) + + if origin_type == 'storage': + table.add_row(['Bucket Name', result['bucketName']]) + + table.add_row(['Origin', result['origin']]) + table.add_row(['Origin Type', result['originType']]) + table.add_row(['Path', result['path']]) + table.add_row(['Port', result['httpPort']]) + table.add_row(['Configuration', result['performanceConfiguration']]) + table.add_row(['Status', result['status']]) + + env.fout(table) diff --git a/SoftLayer/CLI/cdn/origin_list.py b/SoftLayer/CLI/cdn/origin_list.py index 1867a9cdd..208c26f61 100644 --- a/SoftLayer/CLI/cdn/origin_list.py +++ b/SoftLayer/CLI/cdn/origin_list.py @@ -9,20 +9,20 @@ @click.command() -@click.argument('account_id') +@click.argument('unique_id') @environment.pass_env -def cli(env, account_id): - """List origin pull mappings.""" +def cli(env, unique_id): + """List origin path for an existing CDN mapping.""" manager = SoftLayer.CDNManager(env.client) - origins = manager.get_origins(account_id) + origins = manager.get_origins(unique_id) - table = formatting.Table(['id', 'media_type', 'cname', 'origin_url']) + table = formatting.Table(['Path', 'Origin', 'HTTP Port', 'Status']) for origin in origins: - table.add_row([origin['id'], - origin['mediaType'], - origin.get('cname', formatting.blank()), - origin['originUrl']]) + table.add_row([origin['path'], + origin['origin'], + origin['httpPort'], + origin['status']]) env.fout(table) diff --git a/SoftLayer/CLI/cdn/origin_remove.py b/SoftLayer/CLI/cdn/origin_remove.py index 2b8855ede..4e4172387 100644 --- a/SoftLayer/CLI/cdn/origin_remove.py +++ b/SoftLayer/CLI/cdn/origin_remove.py @@ -8,11 +8,13 @@ @click.command() -@click.argument('account_id') -@click.argument('origin_id') +@click.argument('unique_id') +@click.argument('origin_path') @environment.pass_env -def cli(env, account_id, origin_id): - """Remove an origin pull mapping.""" +def cli(env, unique_id, origin_path): + """Removes an origin path for an existing CDN mapping.""" manager = SoftLayer.CDNManager(env.client) - manager.remove_origin(account_id, origin_id) + manager.remove_origin(unique_id, origin_path) + + click.secho("Origin with path %s has been deleted" % origin_path, fg='green') diff --git a/SoftLayer/CLI/cdn/purge.py b/SoftLayer/CLI/cdn/purge.py index 7738600a3..26bff1dd2 100644 --- a/SoftLayer/CLI/cdn/purge.py +++ b/SoftLayer/CLI/cdn/purge.py @@ -5,14 +5,34 @@ import SoftLayer from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting @click.command() -@click.argument('account_id') -@click.argument('content_url', nargs=-1) +@click.argument('unique_id') +@click.argument('path') @environment.pass_env -def cli(env, account_id, content_url): - """Purge cached files from all edge nodes.""" +def cli(env, unique_id, path): + """Creates a purge record and also initiates the purge call. + + Example: + slcli cdn purge 9779455 /article/file.txt + + For more information see the following documentation: \n + https://cloud.ibm.com/docs/infrastructure/CDN?topic=CDN-manage-your-cdn#purging-cached-content + """ manager = SoftLayer.CDNManager(env.client) - manager.purge_content(account_id, content_url) + result = manager.purge_content(unique_id, path) + + table = formatting.Table(['Date', 'Path', 'Saved', 'Status']) + + for data in result: + table.add_row([ + data['date'], + data['path'], + data['saved'], + data['status'] + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/columns.py b/SoftLayer/CLI/columns.py index 50bf89763..8cfdf0bd7 100644 --- a/SoftLayer/CLI/columns.py +++ b/SoftLayer/CLI/columns.py @@ -57,7 +57,7 @@ def mask(self): def get_formatter(columns): """This function returns a callback to use with click options. - The retuend function parses a comma-separated value and returns a new + The returned function parses a comma-separated value and returns a new ColumnFormatter. :param columns: a list of Column instances diff --git a/SoftLayer/CLI/config/setup.py b/SoftLayer/CLI/config/setup.py index cd5a24c1a..c984d569e 100644 --- a/SoftLayer/CLI/config/setup.py +++ b/SoftLayer/CLI/config/setup.py @@ -5,7 +5,6 @@ import click import SoftLayer -from SoftLayer import auth from SoftLayer.CLI import config from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions @@ -20,9 +19,8 @@ def get_api_key(client, username, secret): """ # Try to use a client with username/api key - if len(secret) == 64: + if len(secret) == 64 or username == 'apikey': try: - client.auth = auth.BasicAuthentication(username, secret) client['Account'].getCurrentUser() return secret except SoftLayer.SoftLayerAPIError as ex: @@ -32,24 +30,24 @@ def get_api_key(client, username, secret): # Try to use a client with username/password client.authenticate_with_password(username, secret) - user_record = client['Account'].getCurrentUser( - mask='id, apiAuthenticationKeys') + user_record = client['Account'].getCurrentUser(mask='id, apiAuthenticationKeys') api_keys = user_record['apiAuthenticationKeys'] if len(api_keys) == 0: - return client['User_Customer'].addApiAuthenticationKey( - id=user_record['id']) + return client['User_Customer'].addApiAuthenticationKey(id=user_record['id']) return api_keys[0]['authenticationKey'] @click.command() @environment.pass_env def cli(env): - """Edit configuration.""" + """Setup the ~/.softlayer file with username and apikey. - username, secret, endpoint_url, timeout = get_user_input(env) + Set the username to 'apikey' for cloud.ibm.com accounts. + """ - env.client.transport.transport.endpoint_url = endpoint_url - api_key = get_api_key(env.client, username, secret) + username, secret, endpoint_url, timeout = get_user_input(env) + new_client = SoftLayer.Client(username=username, api_key=secret, endpoint_url=endpoint_url, timeout=timeout) + api_key = get_api_key(new_client, username, secret) path = '~/.softlayer' if env.config_file: @@ -103,17 +101,20 @@ def get_user_input(env): secret = env.getpass('API Key or Password', default=defaults['api_key']) # Ask for which endpoint they want to use + endpoint = defaults.get('endpoint_url', 'public') endpoint_type = env.input( - 'Endpoint (public|private|custom)', default='public') + 'Endpoint (public|private|custom)', default=endpoint) endpoint_type = endpoint_type.lower() - if endpoint_type == 'custom': - endpoint_url = env.input('Endpoint URL', - default=defaults['endpoint_url']) + if endpoint_type == 'public': + endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT elif endpoint_type == 'private': endpoint_url = SoftLayer.API_PRIVATE_ENDPOINT else: - endpoint_url = SoftLayer.API_PUBLIC_ENDPOINT + if endpoint_type == 'custom': + endpoint_url = env.input('Endpoint URL', default=endpoint) + else: + endpoint_url = endpoint_type # Ask for timeout timeout = env.input('Timeout', default=defaults['timeout'] or 0) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index a02bf65a4..a05ffaa54 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -137,7 +137,7 @@ def cli(env, @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 --git a/SoftLayer/CLI/dedicatedhost/cancel.py b/SoftLayer/CLI/dedicatedhost/cancel.py index a01016ec4..58ed85d30 100644 --- a/SoftLayer/CLI/dedicatedhost/cancel.py +++ b/SoftLayer/CLI/dedicatedhost/cancel.py @@ -1,4 +1,4 @@ -"""Cancel a dedicated server.""" +"""Cancel a dedicated host.""" # :license: MIT, see LICENSE for more details. import click @@ -12,17 +12,17 @@ @click.command() @click.argument('identifier') -@click.option('--immediate', - is_flag=True, - default=True, - help="Cancels the server immediately (instead of on the billing anniversary)") -@click.option('--comment', - help="An optional comment to add to the cancellation ticket") -@click.option('--reason', - help="An optional cancellation reason. See cancel-reasons for a list of available options") @environment.pass_env -def cli(env, identifier, immediate, comment, reason): - """Cancel a dedicated server.""" - immediate = True # Enforce immediate cancellation +def cli(env, identifier): + """Cancel a dedicated host server immediately""" + mgr = SoftLayer.DedicatedHostManager(env.client) - mgr.cancel_host(identifier, reason, comment, immediate) + + 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 --git a/SoftLayer/CLI/dedicatedhost/cancel_guests.py b/SoftLayer/CLI/dedicatedhost/cancel_guests.py new file mode 100644 index 000000000..537828de1 --- /dev/null +++ b/SoftLayer/CLI/dedicatedhost/cancel_guests.py @@ -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 --git a/SoftLayer/CLI/dedicatedhost/list_guests.py b/SoftLayer/CLI/dedicatedhost/list_guests.py new file mode 100644 index 000000000..bec37a89f --- /dev/null +++ b/SoftLayer/CLI/dedicatedhost/list_guests.py @@ -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 --git a/SoftLayer/CLI/dns/record_add.py b/SoftLayer/CLI/dns/record_add.py index fbf8213b2..03caf8ff2 100644 --- a/SoftLayer/CLI/dns/record_add.py +++ b/SoftLayer/CLI/dns/record_add.py @@ -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 --git a/SoftLayer/CLI/event_log/__init__.py b/SoftLayer/CLI/event_log/__init__.py new file mode 100644 index 000000000..a10576f5f --- /dev/null +++ b/SoftLayer/CLI/event_log/__init__.py @@ -0,0 +1 @@ +"""Event Logs.""" diff --git a/SoftLayer/CLI/event_log/get.py b/SoftLayer/CLI/event_log/get.py new file mode 100644 index 000000000..3880086f2 --- /dev/null +++ b/SoftLayer/CLI/event_log/get.py @@ -0,0 +1,83 @@ +"""Get Event Logs.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer import utils + + +@click.command() +@click.option('--date-min', '-d', + help='The earliest date we want to search for event logs in mm/dd/yyyy format.') +@click.option('--date-max', '-D', + help='The latest date we want to search for event logs in mm/dd/yyyy format.') +@click.option('--obj-event', '-e', + help="The event we want to get event logs for") +@click.option('--obj-id', '-i', + help="The id of the object we want to get event logs for") +@click.option('--obj-type', '-t', + help="The type of the object we want to get event logs for") +@click.option('--utc-offset', '-z', default='-0000', show_default=True, + help="UTC Offset for searching with dates. +/-HHMM format") +@click.option('--metadata/--no-metadata', default=False, show_default=True, + help="Display metadata if present") +@click.option('--limit', '-l', type=click.INT, default=50, show_default=True, + help="Total number of result to return. -1 to return ALL, there may be a LOT of these.") +@environment.pass_env +def cli(env, date_min, date_max, obj_event, obj_id, obj_type, utc_offset, metadata, limit): + """Get Event Logs + + Example: + slcli event-log get -d 01/01/2019 -D 02/01/2019 -t User -l 10 + """ + columns = ['Event', 'Object', 'Type', 'Date', 'Username'] + + event_mgr = SoftLayer.EventLogManager(env.client) + user_mgr = SoftLayer.UserManager(env.client) + request_filter = event_mgr.build_filter(date_min, date_max, obj_event, obj_id, obj_type, utc_offset) + logs = event_mgr.get_event_logs(request_filter) + log_time = "%Y-%m-%dT%H:%M:%S.%f%z" + user_data = {} + + if metadata: + columns.append('Metadata') + + row_count = 0 + click.secho(", ".join(columns)) + for log in logs: + if log is None: + click.secho('No logs available for filter %s.' % request_filter, fg='red') + return + + user = log['userType'] + label = log.get('label', '') + if user == "CUSTOMER": + username = user_data.get(log['userId']) + if username is None: + username = user_mgr.get_user(log['userId'], "mask[username]")['username'] + user_data[log['userId']] = username + user = username + + if metadata: + metadata_data = log['metaData'].strip("\n\t") + + click.secho("'{0}','{1}','{2}','{3}','{4}','{5}'".format( + log['eventName'], + label, + log['objectName'], + utils.clean_time(log['eventCreateDate'], in_format=log_time), + user, + metadata_data)) + else: + click.secho("'{0}','{1}','{2}','{3}','{4}'".format( + log['eventName'], + label, + log['objectName'], + utils.clean_time(log['eventCreateDate'], in_format=log_time), + user)) + + row_count = row_count + 1 + if row_count >= limit and limit != -1: + return diff --git a/SoftLayer/CLI/event_log/types.py b/SoftLayer/CLI/event_log/types.py new file mode 100644 index 000000000..4bb377e99 --- /dev/null +++ b/SoftLayer/CLI/event_log/types.py @@ -0,0 +1,26 @@ +"""Get Event Log Types.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +COLUMNS = ['types'] + + +@click.command() +@environment.pass_env +def cli(env): + """Get Event Log Types""" + mgr = SoftLayer.EventLogManager(env.client) + + event_log_types = mgr.get_event_log_types() + + table = formatting.Table(COLUMNS) + + for event_log_type in event_log_types: + table.add_row([event_log_type]) + + env.fout(table) diff --git a/SoftLayer/CLI/file/access/authorize.py b/SoftLayer/CLI/file/access/authorize.py index 92fe03653..835f5995f 100644 --- a/SoftLayer/CLI/file/access/authorize.py +++ b/SoftLayer/CLI/file/access/authorize.py @@ -33,11 +33,9 @@ def cli(env, volume_id, hardware_id, virtual_id, ip_address_id, for ip_address_value in ip_address: ip_address_object = network_manager.ip_lookup(ip_address_value) if ip_address_object == "": - click.echo("IP Address not found on your account. " + - "Please confirm IP and try again.") + click.echo("IP Address not found on your account. Please confirm IP and try again.") raise exceptions.ArgumentError('Incorrect IP Address') - else: - ip_address_id_list.append(ip_address_object['id']) + ip_address_id_list.append(ip_address_object['id']) file_manager.authorize_host_to_volume(volume_id, hardware_id, diff --git a/SoftLayer/CLI/file/detail.py b/SoftLayer/CLI/file/detail.py index e34c1d419..02bb5c17a 100644 --- a/SoftLayer/CLI/file/detail.py +++ b/SoftLayer/CLI/file/detail.py @@ -39,7 +39,7 @@ def cli(env, volume_id): table.add_row(['Used Space', "%dGB" % (used_space / (1 << 30))]) if file_volume.get('provisionedIops'): - table.add_row(['IOPs', int(file_volume['provisionedIops'])]) + table.add_row(['IOPs', float(file_volume['provisionedIops'])]) if file_volume.get('storageTierLevel'): table.add_row([ diff --git a/SoftLayer/CLI/hardware/bandwidth.py b/SoftLayer/CLI/hardware/bandwidth.py new file mode 100644 index 000000000..382615052 --- /dev/null +++ b/SoftLayer/CLI/hardware/bandwidth.py @@ -0,0 +1,45 @@ +"""GBandwidth data over date range. Bandwidth is listed in GB""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers +from SoftLayer.CLI.virt.bandwidth import create_bandwidth_table + + +@click.command() +@click.argument('identifier') +@click.option('--start_date', '-s', type=click.STRING, required=True, + help="Start Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss,") +@click.option('--end_date', '-e', type=click.STRING, required=True, + help="End Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss") +@click.option('--summary_period', '-p', type=click.INT, default=3600, show_default=True, + help="300, 600, 1800, 3600, 43200 or 86400 seconds") +@click.option('--quite_summary', '-q', is_flag=True, default=False, show_default=True, + help="Only show the summary table") +@environment.pass_env +def cli(env, identifier, start_date, end_date, summary_period, quite_summary): + """Bandwidth data over date range. Bandwidth is listed in GB + + Using just a date might get you times off by 1 hour, use T00:01 to get just the specific days data + Timezones can also be included with the YYYY-MM-DDTHH:mm:ss.00000-HH:mm format. + + Due to some rounding and date alignment details, results here might be slightly different than + results in the control portal. + + Example:: + + slcli hw bandwidth 1234 -s 2019-05-01T00:01 -e 2019-05-02T00:00:01.00000-12:00 + """ + hardware = SoftLayer.HardwareManager(env.client) + hardware_id = helpers.resolve_id(hardware.resolve_ids, identifier, 'hardware') + data = hardware.get_bandwidth_data(hardware_id, start_date, end_date, None, summary_period) + + title = "Bandwidth Report: %s - %s" % (start_date, end_date) + table, sum_table = create_bandwidth_table(data, summary_period, title) + + env.fout(sum_table) + if not quite_summary: + env.fout(table) diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py index 117ed2d1b..d9a68f8c1 100644 --- a/SoftLayer/CLI/hardware/detail.py +++ b/SoftLayer/CLI/hardware/detail.py @@ -80,6 +80,11 @@ def cli(env, identifier, passwords, price, output_json, verbose): table.add_row(['vlans', vlan_table]) + # Bug in v5.7.2 + # bandwidth = hardware.get_bandwidth_allocation(hardware_id) + # bw_table = _bw_table(bandwidth) + # table.add_row(['Bandwidth', bw_table]) + if result.get('notes'): table.add_row(['notes', result['notes']]) @@ -108,3 +113,17 @@ def cli(env, identifier, passwords, price, output_json, verbose): table.add_row(['tags', formatting.tags(result['tagReferences'])]) env.fout(table) + + +def _bw_table(bw_data): + """Generates a bandwidth useage table""" + table = formatting.Table(['Type', 'In GB', 'Out GB', 'Allotment']) + for bw_point in bw_data.get('useage'): + bw_type = 'Private' + allotment = 'N/A' + if bw_point['type']['alias'] == 'PUBLIC_SERVER_BW': + bw_type = 'Public' + allotment = bw_data['allotment'].get('amount', '-') + + table.add_row([bw_type, bw_point['amountIn'], bw_point['amountOut'], allotment]) + return table diff --git a/SoftLayer/CLI/hardware/edit.py b/SoftLayer/CLI/hardware/edit.py index beca22b3a..e4aca4dcc 100644 --- a/SoftLayer/CLI/hardware/edit.py +++ b/SoftLayer/CLI/hardware/edit.py @@ -12,25 +12,18 @@ @click.command() @click.argument('identifier') @click.option('--domain', '-D', help="Domain portion of the FQDN") -@click.option('--userfile', '-F', - help="Read userdata from file", - type=click.Path(exists=True, readable=True, resolve_path=True)) -@click.option('--tag', '-g', - multiple=True, +@click.option('--userfile', '-F', type=click.Path(exists=True, readable=True, resolve_path=True), + help="Read userdata from file") +@click.option('--tag', '-g', multiple=True, help="Tags to set or empty string to remove all") @click.option('--hostname', '-H', help="Host portion of the FQDN") @click.option('--userdata', '-u', help="User defined metadata string") -@click.option('--public-speed', - help="Public port speed.", - default=None, - type=click.Choice(['0', '10', '100', '1000', '10000'])) -@click.option('--private-speed', - help="Private port speed.", - default=None, - type=click.Choice(['0', '10', '100', '1000', '10000'])) +@click.option('--public-speed', default=None, type=click.Choice(['0', '10', '100', '1000', '10000', '-1']), + help="Public port speed. -1 is best speed available") +@click.option('--private-speed', default=None, type=click.Choice(['0', '10', '100', '1000', '10000', '-1']), + help="Private port speed. -1 is best speed available") @environment.pass_env -def cli(env, identifier, domain, userfile, tag, hostname, userdata, - public_speed, private_speed): +def cli(env, identifier, domain, userfile, tag, hostname, userdata, public_speed, private_speed): """Edit hardware details.""" if userdata and userfile: diff --git a/SoftLayer/CLI/hardware/list.py b/SoftLayer/CLI/hardware/list.py index bb2f210b7..366524264 100644 --- a/SoftLayer/CLI/hardware/list.py +++ b/SoftLayer/CLI/hardware/list.py @@ -22,7 +22,6 @@ 'action', lambda server: formatting.active_txn(server), mask='activeTransaction[id, transactionStatus[name, friendlyName]]'), - column_helper.Column('power_state', ('powerState', 'name')), column_helper.Column( 'created_by', ('billingItem', 'orderItem', 'order', 'userRecord', 'username')), diff --git a/SoftLayer/CLI/hardware/reflash_firmware.py b/SoftLayer/CLI/hardware/reflash_firmware.py new file mode 100644 index 000000000..40d334169 --- /dev/null +++ b/SoftLayer/CLI/hardware/reflash_firmware.py @@ -0,0 +1,26 @@ +"""Reflash firmware.""" +# :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): + """Reflash server firmware.""" + + mgr = SoftLayer.HardwareManager(env.client) + hw_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'hardware') + if not (env.skip_confirmations or + formatting.confirm('This will power off the server with id %s and ' + 'reflash device firmware. Continue?' % hw_id)): + raise exceptions.CLIAbort('Aborted.') + + mgr.reflash_firmware(hw_id) diff --git a/SoftLayer/CLI/hardware/toggle_ipmi.py b/SoftLayer/CLI/hardware/toggle_ipmi.py new file mode 100644 index 000000000..2e49bd72f --- /dev/null +++ b/SoftLayer/CLI/hardware/toggle_ipmi.py @@ -0,0 +1,22 @@ +"""Toggle the IPMI interface on and off.""" +# :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="Whether enable (DEFAULT) or disable the interface.") +@environment.pass_env +def cli(env, identifier, enable): + """Toggle the IPMI interface on and off""" + + mgr = SoftLayer.HardwareManager(env.client) + hw_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'hardware') + result = env.client['Hardware_Server'].toggleManagementInterface(enable, id=hw_id) + env.fout(result) diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index f32595e59..24a5dd445 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -30,17 +30,20 @@ def multi_option(*param_decls, **attrs): def resolve_id(resolver, identifier, name='object'): """Resolves a single id using a resolver function. - :param resolver: function that resolves ids. Should return None or a list - of ids. + :param resolver: function that resolves ids. Should return None or a list of ids. :param string identifier: a string identifier used to resolve ids :param string name: the object type, to be used in error messages """ + try: + return int(identifier) + except ValueError: + pass # It was worth a shot + ids = resolver(identifier) if len(ids) == 0: - raise exceptions.CLIAbort("Error: Unable to find %s '%s'" - % (name, identifier)) + raise exceptions.CLIAbort("Error: Unable to find %s '%s'" % (name, identifier)) if len(ids) > 1: raise exceptions.CLIAbort( diff --git a/SoftLayer/CLI/image/export.py b/SoftLayer/CLI/image/export.py index 327cef475..eb9081ac7 100644 --- a/SoftLayer/CLI/image/export.py +++ b/SoftLayer/CLI/image/export.py @@ -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 --git a/SoftLayer/CLI/image/import.py b/SoftLayer/CLI/image/import.py index 03ec25acb..53082c9ac 100644 --- a/SoftLayer/CLI/image/import.py +++ b/SoftLayer/CLI/image/import.py @@ -16,15 +16,41 @@ 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-crn', + default=None, + help="CRN of the root key in your KMS instance") +@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('--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_crn, wrapped_dek, + 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 +59,12 @@ def cli(env, name, note, os_code, uri): note=note, os_code=os_code, uri=uri, + ibm_api_key=ibm_api_key, + root_key_crn=root_key_crn, + wrapped_dek=wrapped_dek, + cloud_init=cloud_init, + byol=byol, + is_encrypted=is_encrypted ) if not result: diff --git a/SoftLayer/CLI/metadata.py b/SoftLayer/CLI/metadata.py index 1d25ee38b..3cc3e384d 100644 --- a/SoftLayer/CLI/metadata.py +++ b/SoftLayer/CLI/metadata.py @@ -36,8 +36,8 @@ \b Examples : %s -""" % ('*'+'\n*'.join(META_CHOICES), - 'slcli metadata '+'\nslcli metadata '.join(META_CHOICES)) +""" % ('*' + '\n*'.join(META_CHOICES), + 'slcli metadata ' + '\nslcli metadata '.join(META_CHOICES)) @click.command(help=HELP, diff --git a/SoftLayer/CLI/mq/__init__.py b/SoftLayer/CLI/mq/__init__.py deleted file mode 100644 index 3f8947b89..000000000 --- a/SoftLayer/CLI/mq/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Message queue service.""" -# :license: MIT, see LICENSE for more details. - -from SoftLayer.CLI import formatting - - -def queue_table(queue): - """Returns a table with details about a queue.""" - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['name', queue['name']]) - table.add_row(['message_count', queue['message_count']]) - table.add_row(['visible_message_count', queue['visible_message_count']]) - table.add_row(['tags', formatting.listing(queue['tags'] or [])]) - table.add_row(['expiration', queue['expiration']]) - table.add_row(['visibility_interval', queue['visibility_interval']]) - return table - - -def message_table(message): - """Returns a table with details about a message.""" - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['id', message['id']]) - table.add_row(['initial_entry_time', message['initial_entry_time']]) - table.add_row(['visibility_delay', message['visibility_delay']]) - table.add_row(['visibility_interval', message['visibility_interval']]) - table.add_row(['fields', message['fields']]) - return [table, message['body']] - - -def topic_table(topic): - """Returns a table with details about a topic.""" - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['name', topic['name']]) - table.add_row(['tags', formatting.listing(topic['tags'] or [])]) - return table - - -def subscription_table(sub): - """Returns a table with details about a subscription.""" - table = formatting.Table(['property', 'value']) - table.align['property'] = 'r' - table.align['value'] = 'l' - - table.add_row(['id', sub['id']]) - table.add_row(['endpoint_type', sub['endpoint_type']]) - for key, val in sub['endpoint'].items(): - table.add_row([key, val]) - return table diff --git a/SoftLayer/CLI/mq/accounts_list.py b/SoftLayer/CLI/mq/accounts_list.py deleted file mode 100644 index 484881b5b..000000000 --- a/SoftLayer/CLI/mq/accounts_list.py +++ /dev/null @@ -1,28 +0,0 @@ -"""List SoftLayer Message Queue Accounts.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command() -@environment.pass_env -def cli(env): - """List SoftLayer Message Queue Accounts.""" - - manager = SoftLayer.MessagingManager(env.client) - accounts = manager.list_accounts() - - table = formatting.Table(['id', 'name', 'status']) - for account in accounts: - if not account['nodes']: - continue - - table.add_row([account['nodes'][0]['accountName'], - account['name'], - account['status']['name']]) - - env.fout(table) diff --git a/SoftLayer/CLI/mq/endpoints_list.py b/SoftLayer/CLI/mq/endpoints_list.py deleted file mode 100644 index 556642ccd..000000000 --- a/SoftLayer/CLI/mq/endpoints_list.py +++ /dev/null @@ -1,27 +0,0 @@ -"""List SoftLayer Message Queue Endpoints.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command() -@environment.pass_env -def cli(env): - """List SoftLayer Message Queue Endpoints.""" - - manager = SoftLayer.MessagingManager(env.client) - regions = manager.get_endpoints() - - table = formatting.Table(['name', 'public', 'private']) - for region, endpoints in regions.items(): - table.add_row([ - region, - endpoints.get('public') or formatting.blank(), - endpoints.get('private') or formatting.blank(), - ]) - - env.fout(table) diff --git a/SoftLayer/CLI/mq/ping.py b/SoftLayer/CLI/mq/ping.py deleted file mode 100644 index 2a8fa2435..000000000 --- a/SoftLayer/CLI/mq/ping.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Ping the SoftLayer Message Queue service.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import exceptions - - -@click.command() -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, datacenter, network): - """Ping the SoftLayer Message Queue service.""" - - manager = SoftLayer.MessagingManager(env.client) - okay = manager.ping(datacenter=datacenter, network=network) - if okay: - env.fout('OK') - else: - exceptions.CLIAbort('Ping failed') diff --git a/SoftLayer/CLI/mq/queue_add.py b/SoftLayer/CLI/mq/queue_add.py deleted file mode 100644 index 594527116..000000000 --- a/SoftLayer/CLI/mq/queue_add.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Create a queue.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import helpers -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('queue-name') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@click.option('--visibility-interval', - type=click.INT, - default=30, - show_default=True, - help="Time in seconds that messages will re-appear after being " - "popped") -@click.option('--expiration', - type=click.INT, - default=604800, - show_default=True, - help="Time in seconds that messages will live") -@helpers.multi_option('--tag', '-g', help="Tags to add to the queue") -@environment.pass_env -def cli(env, account_id, queue_name, datacenter, network, visibility_interval, - expiration, tag): - """Create a queue.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - - queue = mq_client.create_queue( - queue_name, - visibility_interval=visibility_interval, - expiration=expiration, - tags=tag, - ) - env.fout(mq.queue_table(queue)) diff --git a/SoftLayer/CLI/mq/queue_detail.py b/SoftLayer/CLI/mq/queue_detail.py deleted file mode 100644 index 3cd1694d5..000000000 --- a/SoftLayer/CLI/mq/queue_detail.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Detail a queue.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('queue-name') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, queue_name, datacenter, network): - """Detail a queue.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - queue = mq_client.get_queue(queue_name) - env.fout(mq.queue_table(queue)) diff --git a/SoftLayer/CLI/mq/queue_edit.py b/SoftLayer/CLI/mq/queue_edit.py deleted file mode 100644 index 1f97788bf..000000000 --- a/SoftLayer/CLI/mq/queue_edit.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Modify a queue.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import helpers -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('queue-name') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@click.option('--visibility-interval', - type=click.INT, - default=30, - show_default=True, - help="Time in seconds that messages will re-appear after being " - "popped") -@click.option('--expiration', - type=click.INT, - default=604800, - show_default=True, - help="Time in seconds that messages will live") -@helpers.multi_option('--tag', '-g', help="Tags to add to the queue") -@environment.pass_env -def cli(env, account_id, queue_name, datacenter, network, visibility_interval, - expiration, tag): - """Modify a queue.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - - queue = mq_client.modify_queue( - queue_name, - visibility_interval=visibility_interval, - expiration=expiration, - tags=tag, - ) - env.fout(mq.queue_table(queue)) diff --git a/SoftLayer/CLI/mq/queue_list.py b/SoftLayer/CLI/mq/queue_list.py deleted file mode 100644 index 1059fa3c9..000000000 --- a/SoftLayer/CLI/mq/queue_list.py +++ /dev/null @@ -1,34 +0,0 @@ -"""List all queues on an account.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command() -@click.argument('account-id') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, datacenter, network): - """List all queues on an account.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - - queues = mq_client.get_queues()['items'] - - table = formatting.Table(['name', - 'message_count', - 'visible_message_count']) - for queue in queues: - table.add_row([queue['name'], - queue['message_count'], - queue['visible_message_count']]) - env.fout(table) diff --git a/SoftLayer/CLI/mq/queue_pop.py b/SoftLayer/CLI/mq/queue_pop.py deleted file mode 100644 index 1d81e9ea9..000000000 --- a/SoftLayer/CLI/mq/queue_pop.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Pops a message from a queue.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('queue-name') -@click.option('--count', - default=1, - show_default=True, - type=click.INT, - help="Count of messages to pop") -@click.option('--delete-after', - is_flag=True, - help="Remove popped messages from the queue") -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, queue_name, count, delete_after, datacenter, network): - """Pops a message from a queue.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - - messages = mq_client.pop_messages(queue_name, count) - formatted_messages = [] - for message in messages['items']: - formatted_messages.append(mq.message_table(message)) - - if delete_after: - for message in messages['items']: - mq_client.delete_message(queue_name, message['id']) - env.fout(formatted_messages) diff --git a/SoftLayer/CLI/mq/queue_push.py b/SoftLayer/CLI/mq/queue_push.py deleted file mode 100644 index c025dcf94..000000000 --- a/SoftLayer/CLI/mq/queue_push.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Push a message into a queue.""" -# :license: MIT, see LICENSE for more details. -import sys - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('queue-name') -@click.argument('message') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, queue_name, message, datacenter, network): - """Push a message into a queue.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - body = '' - if message == '-': - body = sys.stdin.read() - else: - body = message - env.fout(mq.message_table(mq_client.push_queue_message(queue_name, body))) diff --git a/SoftLayer/CLI/mq/queue_remove.py b/SoftLayer/CLI/mq/queue_remove.py deleted file mode 100644 index 4396dac1a..000000000 --- a/SoftLayer/CLI/mq/queue_remove.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Delete a queue or a queued message.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - - -@click.command() -@click.argument('account-id') -@click.argument('queue-name') -@click.argument('message-id', required=False) -@click.option('--force', is_flag=True, help="Force the deletion of the queue") -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, queue_name, message_id, force, datacenter, network): - """Delete a queue or a queued message.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - - if message_id: - mq_client.delete_message(queue_name, message_id) - else: - mq_client.delete_queue(queue_name, force) diff --git a/SoftLayer/CLI/mq/topic_add.py b/SoftLayer/CLI/mq/topic_add.py deleted file mode 100644 index 0b48874c2..000000000 --- a/SoftLayer/CLI/mq/topic_add.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Create a new topic.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import helpers -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('topic-name') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@click.option('--visibility-interval', - type=click.INT, - default=30, - show_default=True, - help="Time in seconds that messages will re-appear after being " - "popped") -@click.option('--expiration', - type=click.INT, - default=604800, - show_default=True, - help="Time in seconds that messages will live") -@helpers.multi_option('--tag', '-g', help="Tags to add to the topic") -@environment.pass_env -def cli(env, account_id, topic_name, datacenter, network, - visibility_interval, expiration, tag): - """Create a new topic.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - - topic = mq_client.create_topic( - topic_name, - visibility_interval=visibility_interval, - expiration=expiration, - tags=tag, - ) - env.fout(mq.topic_table(topic)) diff --git a/SoftLayer/CLI/mq/topic_detail.py b/SoftLayer/CLI/mq/topic_detail.py deleted file mode 100644 index bb0b80349..000000000 --- a/SoftLayer/CLI/mq/topic_detail.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Detail a topic.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('topic-name') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, topic_name, datacenter, network): - """Detail a topic.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - topic = mq_client.get_topic(topic_name) - subscriptions = mq_client.get_subscriptions(topic_name) - tables = [] - for sub in subscriptions['items']: - tables.append(mq.subscription_table(sub)) - env.fout([mq.topic_table(topic), tables]) diff --git a/SoftLayer/CLI/mq/topic_list.py b/SoftLayer/CLI/mq/topic_list.py deleted file mode 100644 index f86d13334..000000000 --- a/SoftLayer/CLI/mq/topic_list.py +++ /dev/null @@ -1,29 +0,0 @@ -"""List all topics on an account.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting - - -@click.command() -@click.argument('account-id') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, datacenter, network): - """List all topics on an account.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - topics = mq_client.get_topics()['items'] - - table = formatting.Table(['name']) - for topic in topics: - table.add_row([topic['name']]) - env.fout(table) diff --git a/SoftLayer/CLI/mq/topic_push.py b/SoftLayer/CLI/mq/topic_push.py deleted file mode 100644 index e0384492f..000000000 --- a/SoftLayer/CLI/mq/topic_push.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Push a message into a topic.""" -# :license: MIT, see LICENSE for more details. -import sys - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('topic-name') -@click.argument('message') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, topic_name, message, datacenter, network): - """Push a message into a topic.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - - # the message body comes from the positional argument or stdin - body = '' - if message == '-': - body = sys.stdin.read() - else: - body = message - env.fout(mq.message_table(mq_client.push_topic_message(topic_name, body))) diff --git a/SoftLayer/CLI/mq/topic_remove.py b/SoftLayer/CLI/mq/topic_remove.py deleted file mode 100644 index cdfb22c0f..000000000 --- a/SoftLayer/CLI/mq/topic_remove.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Delete a topic.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - - -@click.command() -@click.argument('account-id') -@click.argument('topic-name') -@click.option('--force', is_flag=True, help="Force the deletion of the queue") -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, topic_name, force, datacenter, network): - """Delete a topic.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - mq_client.delete_topic(topic_name, force) diff --git a/SoftLayer/CLI/mq/topic_subscribe.py b/SoftLayer/CLI/mq/topic_subscribe.py deleted file mode 100644 index 226e20103..000000000 --- a/SoftLayer/CLI/mq/topic_subscribe.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Create a subscription on a topic.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment -from SoftLayer.CLI import mq - - -@click.command() -@click.argument('account-id') -@click.argument('topic-name') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@click.option('--sub-type', - type=click.Choice(['http', 'queue']), - help="Type of endpoint") -@click.option('--queue-name', help="Queue name. Required if --type is queue") -@click.option('--http-method', help="HTTP Method to use if --type is http") -@click.option('--http-url', - help="HTTP/HTTPS URL to use. Required if --type is http") -@click.option('--http-body', - help="HTTP Body template to use if --type is http") -@environment.pass_env -def cli(env, account_id, topic_name, datacenter, network, sub_type, queue_name, - http_method, http_url, http_body): - """Create a subscription on a topic.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - if sub_type == 'queue': - subscription = mq_client.create_subscription(topic_name, 'queue', - queue_name=queue_name) - elif sub_type == 'http': - subscription = mq_client.create_subscription( - topic_name, - 'http', - method=http_method, - url=http_url, - body=http_body, - ) - env.fout(mq.subscription_table(subscription)) diff --git a/SoftLayer/CLI/mq/topic_unsubscribe.py b/SoftLayer/CLI/mq/topic_unsubscribe.py deleted file mode 100644 index 48d4b4940..000000000 --- a/SoftLayer/CLI/mq/topic_unsubscribe.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Remove a subscription on a topic.""" -# :license: MIT, see LICENSE for more details. - -import click - -import SoftLayer -from SoftLayer.CLI import environment - - -@click.command() -@click.argument('account-id') -@click.argument('topic-name') -@click.argument('subscription-id') -@click.option('--datacenter', help="Datacenter, E.G.: dal05") -@click.option('--network', - type=click.Choice(['public', 'private']), - help="Network type") -@environment.pass_env -def cli(env, account_id, topic_name, subscription_id, datacenter, network): - """Remove a subscription on a topic.""" - - manager = SoftLayer.MessagingManager(env.client) - mq_client = manager.get_connection(account_id, - datacenter=datacenter, network=network) - mq_client.delete_subscription(topic_name, subscription_id) diff --git a/SoftLayer/CLI/object_storage/credential/__init__.py b/SoftLayer/CLI/object_storage/credential/__init__.py new file mode 100644 index 000000000..1f71754bb --- /dev/null +++ b/SoftLayer/CLI/object_storage/credential/__init__.py @@ -0,0 +1,42 @@ +"""Manages Object Storage S3 Credentials.""" +# :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 object storage S3 credentials related commands.""" + + 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 object storage credentials S3 related concerns""" diff --git a/SoftLayer/CLI/object_storage/credential/create.py b/SoftLayer/CLI/object_storage/credential/create.py new file mode 100644 index 000000000..934ac7651 --- /dev/null +++ b/SoftLayer/CLI/object_storage/credential/create.py @@ -0,0 +1,28 @@ +"""Create credentials for an IBM Cloud Object Storage Account.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Create credentials for an IBM Cloud Object Storage Account""" + + mgr = SoftLayer.ObjectStorageManager(env.client) + credential = mgr.create_credential(identifier) + table = formatting.Table(['id', 'password', 'username', 'type_name']) + table.sortby = 'id' + table.add_row([ + credential['id'], + credential['password'], + credential['username'], + credential['type']['name'] + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/object_storage/credential/delete.py b/SoftLayer/CLI/object_storage/credential/delete.py new file mode 100644 index 000000000..7b066ba59 --- /dev/null +++ b/SoftLayer/CLI/object_storage/credential/delete.py @@ -0,0 +1,21 @@ +"""Delete the credential of an Object Storage Account.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment + + +@click.command() +@click.argument('identifier') +@click.option('--credential_id', '-c', type=click.INT, + help="This is the credential id associated with the volume") +@environment.pass_env +def cli(env, identifier, credential_id): + """Delete the credential of an Object Storage Account.""" + + mgr = SoftLayer.ObjectStorageManager(env.client) + credential = mgr.delete_credential(identifier, credential_id=credential_id) + + env.fout(credential) diff --git a/SoftLayer/CLI/object_storage/credential/limit.py b/SoftLayer/CLI/object_storage/credential/limit.py new file mode 100644 index 000000000..cc3ad115c --- /dev/null +++ b/SoftLayer/CLI/object_storage/credential/limit.py @@ -0,0 +1,24 @@ +""" Credential limits for this IBM Cloud Object Storage account.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Credential limits for this IBM Cloud Object Storage account.""" + + mgr = SoftLayer.ObjectStorageManager(env.client) + credential_limit = mgr.limit_credential(identifier) + table = formatting.Table(['limit']) + table.add_row([ + credential_limit, + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/object_storage/credential/list.py b/SoftLayer/CLI/object_storage/credential/list.py new file mode 100644 index 000000000..647e4224c --- /dev/null +++ b/SoftLayer/CLI/object_storage/credential/list.py @@ -0,0 +1,29 @@ +"""Retrieve credentials used for generating an AWS signature. Max of 2.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Retrieve credentials used for generating an AWS signature. Max of 2.""" + + mgr = SoftLayer.ObjectStorageManager(env.client) + credential_list = mgr.list_credential(identifier) + table = formatting.Table(['id', 'password', 'username', 'type_name']) + + for credential in credential_list: + table.add_row([ + credential['id'], + credential['password'], + credential['username'], + credential['type']['name'] + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/object_storage/list_accounts.py b/SoftLayer/CLI/object_storage/list_accounts.py index c86ca933f..c49aecc67 100644 --- a/SoftLayer/CLI/object_storage/list_accounts.py +++ b/SoftLayer/CLI/object_storage/list_accounts.py @@ -15,12 +15,19 @@ def cli(env): mgr = SoftLayer.ObjectStorageManager(env.client) accounts = mgr.list_accounts() - table = formatting.Table(['id', 'name']) + table = formatting.Table(['id', 'name', 'apiType']) table.sortby = 'id' + api_type = None for account in accounts: + if 'vendorName' in account and account['vendorName'] == 'Swift': + api_type = 'Swift' + elif 'Cleversafe' in account['serviceResource']['name']: + api_type = 'S3' + table.add_row([ account['id'], account['username'], + api_type, ]) env.fout(table) diff --git a/SoftLayer/CLI/order/__init__.py b/SoftLayer/CLI/order/__init__.py index e69de29bb..7dd721082 100644 --- a/SoftLayer/CLI/order/__init__.py +++ b/SoftLayer/CLI/order/__init__.py @@ -0,0 +1,2 @@ +"""View and order from the catalog.""" +# :license: MIT, see LICENSE for more details. diff --git a/SoftLayer/CLI/order/category_list.py b/SoftLayer/CLI/order/category_list.py index 91671b8e0..9e0ea3a76 100644 --- a/SoftLayer/CLI/order/category_list.py +++ b/SoftLayer/CLI/order/category_list.py @@ -19,18 +19,11 @@ def cli(env, package_keyname, required): """List the categories of a package. - Package keynames can be retrieved from `slcli order package-list` + :: - \b - Example: # List the categories of Bare Metal servers slcli order category-list BARE_METAL_SERVER - When using the --required flag, it will list out only the categories - that are required for ordering that package (see `slcli order item-list`) - - \b - Example: # List the required categories for Bare Metal servers slcli order category-list BARE_METAL_SERVER --required diff --git a/SoftLayer/CLI/order/item_list.py b/SoftLayer/CLI/order/item_list.py index 74f8fd4e7..ad9ae537f 100644 --- a/SoftLayer/CLI/order/item_list.py +++ b/SoftLayer/CLI/order/item_list.py @@ -7,7 +7,7 @@ from SoftLayer.managers import ordering from SoftLayer.utils import lookup -COLUMNS = ['category', 'keyName', 'description'] +COLUMNS = ['category', 'keyName', 'description', 'priceId'] @click.command() @@ -18,29 +18,18 @@ def cli(env, package_keyname, keyword, category): """List package items used for ordering. - The items listed can be used with `slcli order place` to specify + The item keyNames listed can be used with `slcli order place` to specify the items that are being ordered in the package. - Package keynames can be retrieved using `slcli order package-list` - - \b - Note: + .. Note:: Items with a numbered category, like disk0 or gpu0, can be included multiple times in an order to match how many of the item you want to order. - \b - Example: + :: + # List all items in the VSI package slcli order item-list CLOUD_SERVER - The --keyword option is used to filter items by name. - - The --category option is used to filter items by category. - - Both --keyword and --category can be used together. - - \b - Example: # List Ubuntu OSes from the os category of the Bare Metal package slcli order item-list BARE_METAL_SERVER --category os --keyword ubuntu @@ -60,7 +49,7 @@ def cli(env, package_keyname, keyword, category): categories = sorted_items.keys() for catname in sorted(categories): for item in sorted_items[catname]: - table.add_row([catname, item['keyName'], item['description']]) + table.add_row([catname, item['keyName'], item['description'], get_price(item)]) env.fout(table) @@ -75,3 +64,12 @@ def sort_items(items): sorted_items[category].append(item) return sorted_items + + +def get_price(item): + """Given an SoftLayer_Product_Item, returns its default price id""" + + for price in item.get('prices', []): + if not price.get('locationGroupId'): + return price.get('id') + return 0 diff --git a/SoftLayer/CLI/order/package_list.py b/SoftLayer/CLI/order/package_list.py index af3e36269..917af56fe 100644 --- a/SoftLayer/CLI/order/package_list.py +++ b/SoftLayer/CLI/order/package_list.py @@ -7,7 +7,8 @@ from SoftLayer.CLI import formatting from SoftLayer.managers import ordering -COLUMNS = ['name', +COLUMNS = ['id', + 'name', 'keyName', 'type'] @@ -19,23 +20,16 @@ def cli(env, keyword, package_type): """List packages that can be ordered via the placeOrder API. - \b - Example: - # List out all packages for ordering - slcli order package-list + :: - Keywords can also be used for some simple filtering functionality - to help find a package easier. + # List out all packages for ordering + slcli order package-list - \b - Example: # List out all packages with "server" in the name slcli order package-list --keyword server - Package types can be used to remove unwanted packages - \b - Example: + # Select only specifict package types slcli order package-list --package_type BARE_METAL_CPU """ manager = ordering.OrderingManager(env.client) @@ -51,6 +45,7 @@ def cli(env, keyword, package_type): for package in packages: table.add_row([ + package['id'], package['name'], package['keyName'], package['type']['keyName'] diff --git a/SoftLayer/CLI/order/place.py b/SoftLayer/CLI/order/place.py index 1e21e544a..6b66fb110 100644 --- a/SoftLayer/CLI/order/place.py +++ b/SoftLayer/CLI/order/place.py @@ -23,19 +23,23 @@ @click.option('--verify', is_flag=True, help="Flag denoting whether or not to only verify the order, not place it") +@click.option('--quantity', + type=int, + default=1, + help="The quantity of the item being ordered") @click.option('--billing', type=click.Choice(['hourly', 'monthly']), default='hourly', show_default=True, help="Billing rate") -@click.option('--complex-type', help=("The complex type of the order. This typically begins" - " with 'SoftLayer_Container_Product_Order_'.")) +@click.option('--complex-type', + help=("The complex type of the order. Starts 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, verify, billing, complex_type, - extras, order_items): + quantity, extras, order_items): """Place or verify an order. This CLI command is used for placing/verifying an order of the specified package in @@ -43,7 +47,7 @@ def cli(env, package_keyname, location, preset, verify, billing, complex_type, 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) @@ -51,8 +55,9 @@ def cli(env, package_keyname, location, preset, verify, billing, complex_type, 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: + + Example:: + # Order an hourly 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 --billing hourly CLOUD_SERVER DALLAS13 \\ @@ -76,14 +81,17 @@ def cli(env, package_keyname, location, preset, verify, billing, complex_type, manager = ordering.OrderingManager(env.client) if extras: - extras = json.loads(extras) + try: + extras = json.loads(extras) + except ValueError as err: + raise exceptions.CLIAbort("There was an error when parsing the --extras value: {}".format(err)) args = (package_keyname, location, order_items) kwargs = {'preset_keyname': preset, 'extras': extras, - 'quantity': 1, + 'quantity': quantity, 'complex_type': complex_type, - 'hourly': True if billing == 'hourly' else False} + 'hourly': bool(billing == 'hourly')} if verify: table = formatting.Table(COLUMNS) diff --git a/SoftLayer/CLI/order/place_quote.py b/SoftLayer/CLI/order/place_quote.py new file mode 100644 index 000000000..28865ff70 --- /dev/null +++ b/SoftLayer/CLI/order/place_quote.py @@ -0,0 +1,96 @@ +"""Place quote""" +# :license: MIT, see LICENSE for more details. + +import json + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +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 with your user.") +@click.option('--complex-type', + help="The complex type of the order. Starts 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 creating 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`. + + + 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: + try: + extras = json.loads(extras) + except ValueError as err: + raise exceptions.CLIAbort("There was an error when parsing the --extras value: {}".format(err)) + + 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 --git a/SoftLayer/CLI/order/preset_list.py b/SoftLayer/CLI/order/preset_list.py index a619caf77..7397f9428 100644 --- a/SoftLayer/CLI/order/preset_list.py +++ b/SoftLayer/CLI/order/preset_list.py @@ -20,19 +20,15 @@ def cli(env, package_keyname, keyword): """List package presets. - Package keynames can be retrieved from `slcli order package-list`. - Some packages do not have presets. + .. Note:: + Presets are set CPU / RAM / Disk allotments. You still need to specify required items. + Some packages do not have presets. + + :: - \b - Example: # List the presets for Bare Metal servers slcli order preset-list BARE_METAL_SERVER - The --keyword option can also be used for additional filtering on - the returned presets. - - \b - Example: # List the Bare Metal server presets that include a GPU slcli order preset-list BARE_METAL_SERVER --keyword gpu diff --git a/SoftLayer/CLI/order/quote.py b/SoftLayer/CLI/order/quote.py new file mode 100644 index 000000000..498dbdc56 --- /dev/null +++ b/SoftLayer/CLI/order/quote.py @@ -0,0 +1,130 @@ +"""View and Order a quote""" +# :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.managers import ImageManager as ImageManager +from SoftLayer.managers import ordering +from SoftLayer.managers import SshKeyManager as SshKeyManager + + +def _parse_create_args(client, args): + """Converts CLI arguments to args for VSManager.create_instance. + + :param dict args: CLI arguments + """ + data = {} + + if args.get('quantity'): + data['quantity'] = int(args.get('quantity')) + if args.get('postinstall'): + data['provisionScripts'] = [args.get('postinstall')] + if args.get('complex_type'): + data['complexType'] = args.get('complex_type') + + if args.get('fqdn'): + servers = [] + for name in args.get('fqdn'): + fqdn = name.split(".", 1) + servers.append({'hostname': fqdn[0], 'domain': fqdn[1]}) + data['hardware'] = servers + + if args.get('image'): + if args.get('image').isdigit(): + image_mgr = ImageManager(client) + image_details = image_mgr.get_image(args.get('image'), mask="id,globalIdentifier") + data['imageTemplateGlobalIdentifier'] = image_details['globalIdentifier'] + else: + data['imageTemplateGlobalIdentifier'] = args['image'] + + userdata = None + if args.get('userdata'): + userdata = args['userdata'] + elif args.get('userfile'): + with open(args['userfile'], 'r') as userfile: + userdata = userfile.read() + if userdata: + for hardware in data['hardware']: + hardware['userData'] = [{'value': userdata}] + + # Get the SSH keys + if args.get('key'): + keys = [] + for key in args.get('key'): + resolver = SshKeyManager(client).resolve_ids + key_id = helpers.resolve_id(resolver, key, 'SshKey') + keys.append(key_id) + data['sshKeys'] = keys + + return data + + +@click.command() +@click.argument('quote') +@click.option('--verify', is_flag=True, default=False, show_default=True, + help="If specified, will only show what the quote will order, will NOT place an order") +@click.option('--quantity', type=int, default=None, + help="The quantity of the item being ordered if different from quoted value") +@click.option('--complex-type', default='SoftLayer_Container_Product_Order_Hardware_Server', show_default=True, + help=("The complex type of the order. Starts with 'SoftLayer_Container_Product_Order'.")) +@click.option('--userdata', '-u', help="User defined metadata string") +@click.option('--userfile', '-F', type=click.Path(exists=True, readable=True, resolve_path=True), + help="Read userdata from file") +@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('--fqdn', required=True, + help=". formatted name to use. Specify one fqdn per server") +@click.option('--image', help="Image ID. See: 'slcli image list' for reference") +@environment.pass_env +def cli(env, quote, **args): + """View and Order a quote + + \f + :note: + The hostname and domain are split out from the fully qualified domain name. + + If you want to order multiple servers, you need to specify each FQDN. Postinstall, userdata, and + sshkeys are applied to all servers in an order. + + :: + + slcli order quote 12345 --fqdn testing.tester.com \\ + --complex-type SoftLayer_Container_Product_Order_Virtual_Guest -k sshKeyNameLabel\\ + -i https://domain.com/runthis.sh --userdata DataGoesHere + + """ + table = formatting.Table([ + 'Id', 'Name', 'Created', 'Expiration', 'Status' + ]) + create_args = _parse_create_args(env.client, args) + + manager = ordering.OrderingManager(env.client) + quote_details = manager.get_quote_details(quote) + + package = quote_details['order']['items'][0]['package'] + create_args['packageId'] = package['id'] + + if args.get('verify'): + result = manager.verify_quote(quote, create_args) + verify_table = formatting.Table(['keyName', 'description', 'cost']) + verify_table.align['keyName'] = 'l' + verify_table.align['description'] = 'l' + for price in result['prices']: + cost_key = 'hourlyRecurringFee' if result['useHourlyPricing'] is True else 'recurringFee' + verify_table.add_row([ + price['item']['keyName'], + price['item']['description'], + price[cost_key] if cost_key in price else formatting.blank() + ]) + env.fout(verify_table) + else: + result = manager.order_quote(quote, create_args) + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', result['orderId']]) + table.add_row(['created', result['orderDate']]) + table.add_row(['status', result['placedOrder']['status']]) + env.fout(table) diff --git a/SoftLayer/CLI/order/quote_detail.py b/SoftLayer/CLI/order/quote_detail.py new file mode 100644 index 000000000..cef5f41e5 --- /dev/null +++ b/SoftLayer/CLI/order/quote_detail.py @@ -0,0 +1,38 @@ +"""View a quote""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering +from SoftLayer.utils import lookup + + +@click.command() +@click.argument('quote') +@environment.pass_env +def cli(env, quote): + """View a quote""" + + manager = ordering.OrderingManager(env.client) + result = manager.get_quote_details(quote) + + package = result['order']['items'][0]['package'] + title = "{} - Package: {}, Id {}".format(result.get('name'), package['keyName'], package['id']) + table = formatting.Table([ + 'Category', 'Description', 'Quantity', 'Recurring', 'One Time' + ], title=title) + table.align['Category'] = 'l' + table.align['Description'] = 'l' + + items = lookup(result, 'order', 'items') + for item in items: + table.add_row([ + item.get('categoryCode'), + item.get('description'), + item.get('quantity'), + item.get('recurringFee'), + item.get('oneTimeFee') + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/order/quote_list.py b/SoftLayer/CLI/order/quote_list.py new file mode 100644 index 000000000..c43ff7542 --- /dev/null +++ b/SoftLayer/CLI/order/quote_list.py @@ -0,0 +1,36 @@ +"""List active quotes on an account.""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering +from SoftLayer.utils import clean_time + + +@click.command() +@environment.pass_env +def cli(env): + """List all active quotes on an account""" + table = formatting.Table([ + 'Id', 'Name', 'Created', 'Expiration', 'Status', 'Package Name', 'Package Id' + ]) + table.align['Name'] = 'l' + table.align['Package Name'] = 'r' + table.align['Package Id'] = 'l' + + manager = ordering.OrderingManager(env.client) + items = manager.get_quotes() + + for item in items: + package = item['order']['items'][0]['package'] + table.add_row([ + item.get('id'), + item.get('name'), + clean_time(item.get('createDate')), + clean_time(item.get('modifyDate')), + item.get('status'), + package.get('keyName'), + package.get('id') + ]) + env.fout(table) diff --git a/SoftLayer/CLI/report/bandwidth.py b/SoftLayer/CLI/report/bandwidth.py index 47f9fdbc1..f7f28e00c 100644 --- a/SoftLayer/CLI/report/bandwidth.py +++ b/SoftLayer/CLI/report/bandwidth.py @@ -200,7 +200,7 @@ def cli(env, start, end, sortby): table = formatting.Table([ 'type', - 'name', + 'hostname', 'public_in', 'public_out', 'private_in', diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 8279b6246..1eb054bd6 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -11,7 +11,15 @@ ('call-api', 'SoftLayer.CLI.call_api:cli'), + ('account', 'SoftLayer.CLI.account'), + ('account:invoice-detail', 'SoftLayer.CLI.account.invoice_detail:cli'), + ('account:invoices', 'SoftLayer.CLI.account.invoices:cli'), + ('account:events', 'SoftLayer.CLI.account.events:cli'), + ('account:event-detail', 'SoftLayer.CLI.account.event_detail:cli'), + ('account:summary', 'SoftLayer.CLI.account.summary:cli'), + ('virtual', 'SoftLayer.CLI.virt'), + ('virtual:bandwidth', 'SoftLayer.CLI.virt.bandwidth:cli'), ('virtual:cancel', 'SoftLayer.CLI.virt.cancel:cli'), ('virtual:capture', 'SoftLayer.CLI.virt.capture:cli'), ('virtual:create', 'SoftLayer.CLI.virt.create:cli'), @@ -29,7 +37,10 @@ ('virtual:reboot', 'SoftLayer.CLI.virt.power:reboot'), ('virtual:reload', 'SoftLayer.CLI.virt.reload:cli'), ('virtual:upgrade', 'SoftLayer.CLI.virt.upgrade:cli'), + ('virtual:usage', 'SoftLayer.CLI.virt.usage:cli'), ('virtual:credentials', 'SoftLayer.CLI.virt.credentials:cli'), + ('virtual:capacity', 'SoftLayer.CLI.virt.capacity:cli'), + ('virtual:placementgroup', 'SoftLayer.CLI.virt.placementgroup:cli'), ('dedicatedhost', 'SoftLayer.CLI.dedicatedhost'), ('dedicatedhost:list', 'SoftLayer.CLI.dedicatedhost.list:cli'), @@ -37,12 +48,13 @@ ('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'), ('dedicatedhost:edit', 'SoftLayer.CLI.dedicatedhost.edit:cli'), ('cdn', 'SoftLayer.CLI.cdn'), ('cdn:detail', 'SoftLayer.CLI.cdn.detail:cli'), ('cdn:list', 'SoftLayer.CLI.cdn.list:cli'), - ('cdn:load', 'SoftLayer.CLI.cdn.load:cli'), ('cdn:origin-add', 'SoftLayer.CLI.cdn.origin_add:cli'), ('cdn:origin-list', 'SoftLayer.CLI.cdn.origin_list:cli'), ('cdn:origin-remove', 'SoftLayer.CLI.cdn.origin_remove:cli'), @@ -73,15 +85,13 @@ ('block:replica-failover', 'SoftLayer.CLI.block.replication.failover:cli'), ('block:replica-order', 'SoftLayer.CLI.block.replication.order:cli'), ('block:replica-partners', 'SoftLayer.CLI.block.replication.partners:cli'), - ('block:replica-locations', - 'SoftLayer.CLI.block.replication.locations:cli'), + ('block:replica-locations', 'SoftLayer.CLI.block.replication.locations:cli'), ('block:snapshot-cancel', 'SoftLayer.CLI.block.snapshot.cancel:cli'), ('block:snapshot-create', 'SoftLayer.CLI.block.snapshot.create:cli'), ('block:snapshot-delete', 'SoftLayer.CLI.block.snapshot.delete:cli'), ('block:snapshot-disable', 'SoftLayer.CLI.block.snapshot.disable:cli'), ('block:snapshot-enable', 'SoftLayer.CLI.block.snapshot.enable:cli'), - ('block:snapshot-schedule-list', - 'SoftLayer.CLI.block.snapshot.schedule_list:cli'), + ('block:snapshot-schedule-list', 'SoftLayer.CLI.block.snapshot.schedule_list:cli'), ('block:snapshot-list', 'SoftLayer.CLI.block.snapshot.list:cli'), ('block:snapshot-order', 'SoftLayer.CLI.block.snapshot.order:cli'), ('block:snapshot-restore', 'SoftLayer.CLI.block.snapshot.restore:cli'), @@ -94,6 +104,10 @@ ('block:volume-order', 'SoftLayer.CLI.block.order:cli'), ('block:volume-set-lun-id', 'SoftLayer.CLI.block.lun:cli'), + ('event-log', 'SoftLayer.CLI.event_log'), + ('event-log:get', 'SoftLayer.CLI.event_log.get:cli'), + ('event-log:types', 'SoftLayer.CLI.event_log.types:cli'), + ('file', 'SoftLayer.CLI.file'), ('file:access-authorize', 'SoftLayer.CLI.file.access.authorize:cli'), ('file:access-list', 'SoftLayer.CLI.file.access.list:cli'), @@ -108,8 +122,7 @@ ('file:snapshot-delete', 'SoftLayer.CLI.file.snapshot.delete:cli'), ('file:snapshot-disable', 'SoftLayer.CLI.file.snapshot.disable:cli'), ('file:snapshot-enable', 'SoftLayer.CLI.file.snapshot.enable:cli'), - ('file:snapshot-schedule-list', - 'SoftLayer.CLI.file.snapshot.schedule_list:cli'), + ('file:snapshot-schedule-list', 'SoftLayer.CLI.file.snapshot.schedule_list:cli'), ('file:snapshot-list', 'SoftLayer.CLI.file.snapshot.list:cli'), ('file:snapshot-order', 'SoftLayer.CLI.file.snapshot.order:cli'), ('file:snapshot-restore', 'SoftLayer.CLI.file.snapshot.restore:cli'), @@ -150,10 +163,8 @@ ('ipsec:subnet-add', 'SoftLayer.CLI.vpn.ipsec.subnet.add:cli'), ('ipsec:subnet-remove', 'SoftLayer.CLI.vpn.ipsec.subnet.remove:cli'), ('ipsec:translation-add', 'SoftLayer.CLI.vpn.ipsec.translation.add:cli'), - ('ipsec:translation-remove', - 'SoftLayer.CLI.vpn.ipsec.translation.remove:cli'), - ('ipsec:translation-update', - 'SoftLayer.CLI.vpn.ipsec.translation.update:cli'), + ('ipsec:translation-remove', 'SoftLayer.CLI.vpn.ipsec.translation.remove:cli'), + ('ipsec:translation-update', 'SoftLayer.CLI.vpn.ipsec.translation.update:cli'), ('ipsec:update', 'SoftLayer.CLI.vpn.ipsec.update:cli'), ('loadbal', 'SoftLayer.CLI.loadbal'), @@ -174,25 +185,6 @@ ('loadbal:service-edit', 'SoftLayer.CLI.loadbal.service_edit:cli'), ('loadbal:service-toggle', 'SoftLayer.CLI.loadbal.service_toggle:cli'), - ('messaging', 'SoftLayer.CLI.mq'), - ('messaging:accounts-list', 'SoftLayer.CLI.mq.accounts_list:cli'), - ('messaging:endpoints-list', 'SoftLayer.CLI.mq.endpoints_list:cli'), - ('messaging:ping', 'SoftLayer.CLI.mq.ping:cli'), - ('messaging:queue-add', 'SoftLayer.CLI.mq.queue_add:cli'), - ('messaging:queue-detail', 'SoftLayer.CLI.mq.queue_detail:cli'), - ('messaging:queue-edit', 'SoftLayer.CLI.mq.queue_edit:cli'), - ('messaging:queue-list', 'SoftLayer.CLI.mq.queue_list:cli'), - ('messaging:queue-pop', 'SoftLayer.CLI.mq.queue_pop:cli'), - ('messaging:queue-push', 'SoftLayer.CLI.mq.queue_push:cli'), - ('messaging:queue-remove', 'SoftLayer.CLI.mq.queue_remove:cli'), - ('messaging:topic-add', 'SoftLayer.CLI.mq.topic_add:cli'), - ('messaging:topic-detail', 'SoftLayer.CLI.mq.topic_detail:cli'), - ('messaging:topic-list', 'SoftLayer.CLI.mq.topic_list:cli'), - ('messaging:topic-push', 'SoftLayer.CLI.mq.topic_push:cli'), - ('messaging:topic-remove', 'SoftLayer.CLI.mq.topic_remove:cli'), - ('messaging:topic-subscribe', 'SoftLayer.CLI.mq.topic_subscribe:cli'), - ('messaging:topic-unsubscribe', 'SoftLayer.CLI.mq.topic_unsubscribe:cli'), - ('metadata', 'SoftLayer.CLI.metadata:cli'), ('nas', 'SoftLayer.CLI.nas'), @@ -203,10 +195,10 @@ ('nas:revoke-access', 'SoftLayer.CLI.nas.revoke_access:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), - ('object-storage:accounts', - 'SoftLayer.CLI.object_storage.list_accounts:cli'), - ('object-storage:endpoints', - 'SoftLayer.CLI.object_storage.list_endpoints:cli'), + + ('object-storage:accounts', 'SoftLayer.CLI.object_storage.list_accounts:cli'), + ('object-storage:endpoints', 'SoftLayer.CLI.object_storage.list_endpoints:cli'), + ('object-storage:credential', 'SoftLayer.CLI.object_storage.credential:cli'), ('order', 'SoftLayer.CLI.order'), ('order:category-list', 'SoftLayer.CLI.order.category_list:cli'), @@ -215,12 +207,17 @@ ('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'), + ('order:quote-list', 'SoftLayer.CLI.order.quote_list:cli'), + ('order:quote-detail', 'SoftLayer.CLI.order.quote_detail:cli'), + ('order:quote', 'SoftLayer.CLI.order.quote:cli'), ('rwhois', 'SoftLayer.CLI.rwhois'), ('rwhois:edit', 'SoftLayer.CLI.rwhois.edit:cli'), ('rwhois:show', 'SoftLayer.CLI.rwhois.show:cli'), ('hardware', 'SoftLayer.CLI.hardware'), + ('hardware:bandwidth', 'SoftLayer.CLI.hardware.bandwidth:cli'), ('hardware:cancel', 'SoftLayer.CLI.hardware.cancel:cli'), ('hardware:cancel-reasons', 'SoftLayer.CLI.hardware.cancel_reasons:cli'), ('hardware:create', 'SoftLayer.CLI.hardware.create:cli'), @@ -235,8 +232,10 @@ ('hardware:reload', 'SoftLayer.CLI.hardware.reload:cli'), ('hardware:credentials', 'SoftLayer.CLI.hardware.credentials:cli'), ('hardware:update-firmware', 'SoftLayer.CLI.hardware.update_firmware:cli'), + ('hardware:reflash-firmware', 'SoftLayer.CLI.hardware.reflash_firmware:cli'), ('hardware:rescue', 'SoftLayer.CLI.hardware.power:rescue'), ('hardware:ready', 'SoftLayer.CLI.hardware.ready:cli'), + ('hardware:toggle-ipmi', 'SoftLayer.CLI.hardware.toggle_ipmi:cli'), ('hardware:status', 'SoftLayer.CLI.hardware.status:cli'), ('securitygroup', 'SoftLayer.CLI.securitygroup'), @@ -255,6 +254,7 @@ 'SoftLayer.CLI.securitygroup.interface:add'), ('securitygroup:interface-remove', 'SoftLayer.CLI.securitygroup.interface:remove'), + ('securitygroup:event-log', 'SoftLayer.CLI.securitygroup.event_log:get_by_request_id'), ('sshkey', 'SoftLayer.CLI.sshkey'), ('sshkey:add', 'SoftLayer.CLI.sshkey.add:cli'), @@ -317,4 +317,5 @@ 'vm': 'virtual', 'vs': 'virtual', 'dh': 'dedicatedhost', + 'pg': 'placementgroup', } diff --git a/SoftLayer/CLI/securitygroup/event_log.py b/SoftLayer/CLI/securitygroup/event_log.py new file mode 100644 index 000000000..fbf109d9b --- /dev/null +++ b/SoftLayer/CLI/securitygroup/event_log.py @@ -0,0 +1,32 @@ +"""Get event logs relating to security groups""" +# :license: MIT, see LICENSE for more details. + +import json + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +COLUMNS = ['event', 'label', 'date', 'metadata'] + + +@click.command() +@click.argument('request_id') +@environment.pass_env +def get_by_request_id(env, request_id): + """Search for event logs by request id""" + mgr = SoftLayer.NetworkManager(env.client) + + logs = mgr.get_event_logs_by_request_id(request_id) + + table = formatting.Table(COLUMNS) + table.align['metadata'] = "l" + + for log in logs: + metadata = json.dumps(json.loads(log['metaData']), indent=4, sort_keys=True) + + table.add_row([log['eventName'], log['label'], log['eventCreateDate'], metadata]) + + env.fout(table) diff --git a/SoftLayer/CLI/securitygroup/interface.py b/SoftLayer/CLI/securitygroup/interface.py index a2f33ee87..db07ae851 100644 --- a/SoftLayer/CLI/securitygroup/interface.py +++ b/SoftLayer/CLI/securitygroup/interface.py @@ -14,6 +14,8 @@ 'interface', 'ipAddress', ] +REQUEST_COLUMNS = ['requestId'] + @click.command() @click.argument('securitygroup_id') @@ -90,11 +92,16 @@ def add(env, securitygroup_id, network_component, server, interface): mgr = SoftLayer.NetworkManager(env.client) component_id = _get_component_id(env, network_component, server, interface) - success = mgr.attach_securitygroup_component(securitygroup_id, - component_id) - if not success: + ret = mgr.attach_securitygroup_component(securitygroup_id, + component_id) + if not ret: raise exceptions.CLIAbort("Could not attach network component") + table = formatting.Table(REQUEST_COLUMNS) + table.add_row([ret['requestId']]) + + env.fout(table) + @click.command() @click.argument('securitygroup_id') @@ -113,11 +120,16 @@ def remove(env, securitygroup_id, network_component, server, interface): mgr = SoftLayer.NetworkManager(env.client) component_id = _get_component_id(env, network_component, server, interface) - success = mgr.detach_securitygroup_component(securitygroup_id, - component_id) - if not success: + ret = mgr.detach_securitygroup_component(securitygroup_id, + component_id) + if not ret: raise exceptions.CLIAbort("Could not detach network component") + table = formatting.Table(REQUEST_COLUMNS) + table.add_row([ret['requestId']]) + + env.fout(table) + def _validate_args(network_component, server, interface): use_server = bool(server and interface and not network_component) diff --git a/SoftLayer/CLI/securitygroup/rule.py b/SoftLayer/CLI/securitygroup/rule.py index 4f624308c..6d6c33c62 100644 --- a/SoftLayer/CLI/securitygroup/rule.py +++ b/SoftLayer/CLI/securitygroup/rule.py @@ -15,7 +15,12 @@ 'ethertype', 'portRangeMin', 'portRangeMax', - 'protocol'] + 'protocol', + 'createDate', + 'modifyDate'] + +REQUEST_BOOL_COLUMNS = ['requestId', 'response'] +REQUEST_RULES_COLUMNS = ['requestId', 'rules'] @click.command() @@ -49,7 +54,9 @@ def rule_list(env, securitygroup_id, sortby): 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) @@ -105,6 +112,11 @@ def add(env, securitygroup_id, remote_ip, remote_group, if not ret: raise exceptions.CLIAbort("Failed to add security group rule") + table = formatting.Table(REQUEST_RULES_COLUMNS) + table.add_row([ret['requestId'], str(ret['rules'])]) + + env.fout(table) + @click.command() @click.argument('securitygroup_id') @@ -145,9 +157,16 @@ def edit(env, securitygroup_id, rule_id, remote_ip, remote_group, if protocol: data['protocol'] = protocol - if not mgr.edit_securitygroup_rule(securitygroup_id, rule_id, **data): + ret = mgr.edit_securitygroup_rule(securitygroup_id, rule_id, **data) + + if not ret: raise exceptions.CLIAbort("Failed to edit security group rule") + table = formatting.Table(REQUEST_BOOL_COLUMNS) + table.add_row([ret['requestId']]) + + env.fout(table) + @click.command() @click.argument('securitygroup_id') @@ -156,5 +175,13 @@ def edit(env, securitygroup_id, rule_id, remote_ip, remote_group, def remove(env, securitygroup_id, rule_id): """Remove a rule from a security group.""" mgr = SoftLayer.NetworkManager(env.client) - if not mgr.remove_securitygroup_rule(securitygroup_id, rule_id): + + ret = mgr.remove_securitygroup_rule(securitygroup_id, rule_id) + + if not ret: raise exceptions.CLIAbort("Failed to remove security group rule") + + table = formatting.Table(REQUEST_BOOL_COLUMNS) + table.add_row([ret['requestId']]) + + env.fout(table) diff --git a/SoftLayer/CLI/subnet/create.py b/SoftLayer/CLI/subnet/create.py index 0f81c574b..5918c5859 100644 --- a/SoftLayer/CLI/subnet/create.py +++ b/SoftLayer/CLI/subnet/create.py @@ -10,25 +10,33 @@ @click.command(short_help="Add a new subnet to your account") -@click.argument('network', type=click.Choice(['public', 'private'])) +@click.argument('network', type=click.Choice(['static', 'public', 'private'])) @click.argument('quantity', type=click.INT) -@click.argument('vlan-id') -@click.option('--v6', '--ipv6', is_flag=True, help="Order IPv6 Addresses") +@click.argument('endpoint-id', type=click.INT) +@click.option('--ipv6', '--v6', is_flag=True, help="Order IPv6 Addresses") @click.option('--test', is_flag=True, help="Do not order the subnet; just get a quote") @environment.pass_env -def cli(env, network, quantity, vlan_id, ipv6, test): +def cli(env, network, quantity, endpoint_id, ipv6, test): """Add a new subnet to your account. Valid quantities vary by type. \b - Type - Valid Quantities (IPv4) - public - 4, 8, 16, 32 - private - 4, 8, 16, 32, 64 + IPv4 + static - 1, 2, 4, 8, 16, 32, 64, 128, 256 + public - 4, 8, 16, 32, 64, 128, 256 + private - 4, 8, 16, 32, 64, 128, 256 \b - Type - Valid Quantities (IPv6) + IPv6 + static - 64 public - 64 + + \b + endpoint-id + static - Network_Subnet_IpAddress identifier. + public - Network_Vlan identifier + private - Network_Vlan identifier """ mgr = SoftLayer.NetworkManager(env.client) @@ -42,14 +50,14 @@ def cli(env, network, quantity, vlan_id, ipv6, test): if ipv6: version = 6 - result = mgr.add_subnet(network, - quantity=quantity, - vlan_id=vlan_id, - version=version, - test_order=test) - if not result: - raise exceptions.CLIAbort( - 'Unable to place order: No valid price IDs found.') + try: + result = mgr.add_subnet(network, quantity=quantity, endpoint_id=endpoint_id, version=version, test_order=test) + + except SoftLayer.SoftLayerAPIError as error: + raise exceptions.CLIAbort('Unable to order {} {} ipv{} , error: {}'.format(quantity, + network, + version, + error.faultString)) table = formatting.Table(['Item', 'cost']) table.align['Item'] = 'r' diff --git a/SoftLayer/CLI/subnet/list.py b/SoftLayer/CLI/subnet/list.py index e5d0d83a3..508d649a2 100644 --- a/SoftLayer/CLI/subnet/list.py +++ b/SoftLayer/CLI/subnet/list.py @@ -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 --git a/SoftLayer/CLI/ticket/attach.py b/SoftLayer/CLI/ticket/attach.py index 98adaa65b..c3086659f 100644 --- a/SoftLayer/CLI/ticket/attach.py +++ b/SoftLayer/CLI/ticket/attach.py @@ -11,11 +11,9 @@ @click.command() @click.argument('identifier', type=int) -@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") @environment.pass_env def cli(env, identifier, hardware_identifier, virtual_identifier): @@ -23,20 +21,15 @@ def cli(env, identifier, hardware_identifier, virtual_identifier): ticket_mgr = SoftLayer.TicketManager(env.client) if hardware_identifier and virtual_identifier: - raise exceptions.ArgumentError( - "Cannot attach hardware and a virtual server at the same time") - elif hardware_identifier: + raise exceptions.ArgumentError("Cannot attach hardware and a virtual server at the same time") + + 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(identifier, hardware_id) elif 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(identifier, vs_id) else: - raise exceptions.ArgumentError( - "Must have a hardware or virtual server identifier to attach") + raise exceptions.ArgumentError("Must have a hardware or virtual server identifier to attach") diff --git a/SoftLayer/CLI/ticket/detach.py b/SoftLayer/CLI/ticket/detach.py index 8c8cae058..94a6f72ea 100644 --- a/SoftLayer/CLI/ticket/detach.py +++ b/SoftLayer/CLI/ticket/detach.py @@ -11,11 +11,9 @@ @click.command() @click.argument('identifier', type=int) -@click.option('--hardware', - 'hardware_identifier', +@click.option('--hardware', 'hardware_identifier', help="The identifier for hardware to detach") -@click.option('--virtual', - 'virtual_identifier', +@click.option('--virtual', 'virtual_identifier', help="The identifier for a virtual server to detach") @environment.pass_env def cli(env, identifier, hardware_identifier, virtual_identifier): @@ -23,20 +21,15 @@ def cli(env, identifier, hardware_identifier, virtual_identifier): ticket_mgr = SoftLayer.TicketManager(env.client) if hardware_identifier and virtual_identifier: - raise exceptions.ArgumentError( - "Cannot detach hardware and a virtual server at the same time") - elif hardware_identifier: + raise exceptions.ArgumentError("Cannot detach hardware and a virtual server at the same time") + + 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.detach_hardware(identifier, hardware_id) elif 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.detach_virtual_server(identifier, vs_id) else: - raise exceptions.ArgumentError( - "Must have a hardware or virtual server identifier to detach") + raise exceptions.ArgumentError("Must have a hardware or virtual server identifier to detach") diff --git a/SoftLayer/CLI/user/detail.py b/SoftLayer/CLI/user/detail.py index 498874482..11b55546a 100644 --- a/SoftLayer/CLI/user/detail.py +++ b/SoftLayer/CLI/user/detail.py @@ -23,7 +23,7 @@ @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.") + help="Show event log for this user.") @environment.pass_env def cli(env, identifier, keys, permissions, hardware, virtual, logins, events): """User details.""" diff --git a/SoftLayer/CLI/virt/bandwidth.py b/SoftLayer/CLI/virt/bandwidth.py new file mode 100644 index 000000000..2f29cc7f8 --- /dev/null +++ b/SoftLayer/CLI/virt/bandwidth.py @@ -0,0 +1,103 @@ +"""Get details for a hardware device.""" +# :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('--start_date', '-s', type=click.STRING, required=True, + help="Start Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss,") +@click.option('--end_date', '-e', type=click.STRING, required=True, + help="End Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss") +@click.option('--summary_period', '-p', type=click.INT, default=3600, show_default=True, + help="300, 600, 1800, 3600, 43200 or 86400 seconds") +@click.option('--quite_summary', '-q', is_flag=True, default=False, show_default=True, + help="Only show the summary table") +@environment.pass_env +def cli(env, identifier, start_date, end_date, summary_period, quite_summary): + """Bandwidth data over date range. Bandwidth is listed in GB + + Using just a date might get you times off by 1 hour, use T00:01 to get just the specific days data + Timezones can also be included with the YYYY-MM-DDTHH:mm:ss.00000-HH:mm format. + + Due to some rounding and date alignment details, results here might be slightly different than + results in the control portal. + + Example:: + + slcli hw bandwidth 1234 -s 2019-05-01T00:01 -e 2019-05-02T00:00:01.00000-12:00 + """ + vsi = SoftLayer.VSManager(env.client) + vsi_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS') + data = vsi.get_bandwidth_data(vsi_id, start_date, end_date, None, summary_period) + + title = "Bandwidth Report: %s - %s" % (start_date, end_date) + table, sum_table = create_bandwidth_table(data, summary_period, title) + + env.fout(sum_table) + if not quite_summary: + env.fout(table) + + +def create_bandwidth_table(data, summary_period, title="Bandwidth Report"): + """Create 2 tables, bandwidth and sumamry. Used here and in hw bandwidth command""" + + formatted_data = {} + for point in data: + key = utils.clean_time(point['dateTime']) + data_type = point['type'] + # conversion from byte to megabyte + value = round(float(point['counter']) / 2 ** 20, 4) + if formatted_data.get(key) is None: + formatted_data[key] = {} + formatted_data[key][data_type] = float(value) + + table = formatting.Table(['Date', 'Pub In', 'Pub Out', 'Pri In', 'Pri Out'], title=title) + + sum_table = formatting.Table(['Type', 'Sum GB', 'Average MBps', 'Max GB', 'Max Date'], title="Summary") + + # Required to specify keyName because getBandwidthTotals returns other counter types for some reason. + bw_totals = [ + {'keyName': 'publicIn_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pub In'}, + {'keyName': 'publicOut_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pub Out'}, + {'keyName': 'privateIn_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pri In'}, + {'keyName': 'privateOut_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pri Out'}, + ] + + for point in formatted_data: + new_row = [point] + for bw_type in bw_totals: + counter = formatted_data[point].get(bw_type['keyName'], 0) + new_row.append(mb_to_gb(counter)) + bw_type['sum'] = bw_type['sum'] + counter + if counter > bw_type['max']: + bw_type['max'] = counter + bw_type['maxDate'] = point + table.add_row(new_row) + + for bw_type in bw_totals: + total = bw_type.get('sum', 0.0) + average = 0 + if total > 0: + average = round(total / len(formatted_data) / summary_period, 4) + sum_table.add_row([ + bw_type.get('name'), + mb_to_gb(total), + average, + mb_to_gb(bw_type.get('max')), + bw_type.get('maxDate') + ]) + + return table, sum_table + + +def mb_to_gb(mbytes): + """Converts a MegaByte int to GigaByte. mbytes/2^10""" + return round(mbytes / 2 ** 10, 4) diff --git a/SoftLayer/CLI/virt/capacity/__init__.py b/SoftLayer/CLI/virt/capacity/__init__.py new file mode 100644 index 000000000..3f891c194 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/__init__.py @@ -0,0 +1,47 @@ +"""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""" diff --git a/SoftLayer/CLI/virt/capacity/create.py b/SoftLayer/CLI/virt/capacity/create.py new file mode 100644 index 000000000..92da7745c --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/create.py @@ -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 --git a/SoftLayer/CLI/virt/capacity/create_guest.py b/SoftLayer/CLI/virt/capacity/create_guest.py new file mode 100644 index 000000000..4b8a983a6 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/create_guest.py @@ -0,0 +1,62 @@ +"""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) + + 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 --git a/SoftLayer/CLI/virt/capacity/create_options.py b/SoftLayer/CLI/virt/capacity/create_options.py new file mode 100644 index 000000000..14203cb48 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/create_options.py @@ -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 --git a/SoftLayer/CLI/virt/capacity/detail.py b/SoftLayer/CLI/virt/capacity/detail.py new file mode 100644 index 000000000..60dc644f8 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/detail.py @@ -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 --git a/SoftLayer/CLI/virt/capacity/list.py b/SoftLayer/CLI/virt/capacity/list.py new file mode 100644 index 000000000..a5a5445c1 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/list.py @@ -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 --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index 2026909df..578f5d523 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -33,6 +33,8 @@ def _update_with_like_args(ctx, _, value): 'postinstall': like_details.get('postInstallScriptUri'), 'dedicated': like_details['dedicatedAccountHostOnlyFlag'], 'private': like_details['privateNetworkOnlyFlag'], + 'placement_id': like_details.get('placementGroupId', None), + 'transient': like_details.get('transientGuestFlag', None), } like_args['flavor'] = utils.lookup(like_details, @@ -72,20 +74,32 @@ def _parse_create_args(client, args): :param dict args: CLI arguments """ + config_list = [] for hostname in args['hostnames'].split(','): data = { - "hourly": args['billing'] == 'hourly', - "domain": args['domain'], - "hostname": hostname, - "private": args['private'], - "dedicated": args['dedicated'], - "disks": args['disk'], + "hourly": args.get('billing', 'hourly') == 'hourly', "cpus": args.get('cpu', None), + "ipv6": args.get('ipv6', None), + "disks": args.get('disk', None), + "os_code": args.get('os', None), "memory": args.get('memory', None), "flavor": args.get('flavor', None), - "boot_mode": args.get('boot_mode', None) + "domain": args.get('domain', None), + "host_id": args.get('host_id', None), + "private": args.get('private', None), + "transient": args.get('transient', None), + "hostname": hostname, + "nic_speed": args.get('network', None), + "boot_mode": args.get('boot_mode', None), + "dedicated": args.get('dedicated', None), + "post_uri": args.get('postinstall', None), + "datacenter": args.get('datacenter', None), + "public_vlan": args.get('vlan_public', None), + "private_vlan": args.get('vlan_private', None), + "public_subnet": args.get('subnet_public', None), + "private_subnet": args.get('subnet_private', None), } # The primary disk is included in the flavor and the local_disk flag is not needed @@ -93,10 +107,7 @@ def _parse_create_args(client, args): if not args.get('san') and args.get('flavor'): data['local_disk'] = None else: - data['local_disk'] = not args['san'] - - if args.get('os'): - data['os_code'] = args['os'] + data['local_disk'] = not args.get('san') if args.get('image'): if args.get('image').isdigit(): @@ -105,13 +116,7 @@ def _parse_create_args(client, args): mask="id,globalIdentifier") data['image_id'] = image_details['globalIdentifier'] else: - data['image_id'] = args['image'] - - if args.get('datacenter'): - data['datacenter'] = args['datacenter'] - - if args.get('network'): - data['nic_speed'] = args.get('network') + data['local_disk'] = not args['san'] if args.get('userdata'): data['userdata'] = args['userdata'] @@ -119,10 +124,7 @@ def _parse_create_args(client, args): with open(args['userfile'], 'r') as userfile: data['userdata'] = userfile.read() - if args.get('postinstall'): - data['post_uri'] = args.get('postinstall') - - # Get the SSH keys + # Get the SSH keys if args.get('key'): keys = [] for key in args.get('key'): @@ -131,20 +133,9 @@ def _parse_create_args(client, args): keys.append(key_id) data['ssh_keys'] = keys - if args.get('vlan_public'): - data['public_vlan'] = args['vlan_public'] - - 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] - if args.get('private_security_group'): priv_groups = args.get('private_security_group') data['private_security_groups'] = [group for group in priv_groups] @@ -152,83 +143,70 @@ def _parse_create_args(client, args): if args.get('tag'): data['tags'] = ','.join(args['tag']) - if args.get('host_id'): - data['host_id'] = args['host_id'] + if args.get('placementgroup'): + resolver = SoftLayer.managers.PlacementManager(client).resolve_ids + data['placement_id'] = helpers.resolve_id(resolver, args.get('placementgroup'), 'PlacementGroup') config_list.append(data) return config_list @click.command(epilog="See 'slcli vs create-options' for valid options") +@click.option('--domain', '-D', required=True, prompt=True, help="Domain portion of the FQDN") +@click.option('--cpu', '-c', type=click.INT, help="Number of CPU cores (not available with flavors)") +@click.option('--memory', '-m', type=virt.MEM_TYPE, help="Memory in mebibytes (not available with flavors)") +@click.option('--flavor', '-f', type=click.STRING, help="Public Virtual Server flavor key name") +@click.option('--datacenter', '-d', required=True, prompt=True, help="Datacenter shortname") +@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('--billing', type=click.Choice(['hourly', 'monthly']), default='hourly', show_default=True, help="Billing rate") @click.option('--hostnames', '-H', help="Hosts portion of the FQDN", required=True, prompt=True) -@click.option('--domain', '-D', - help="Domain portion of the FQDN", - required=True, - prompt=True) -@click.option('--cpu', '-c', - help="Number of CPU cores (not available with flavors)", - type=click.INT) -@click.option('--memory', '-m', - help="Memory in mebibytes (not available with flavors)", - type=virt.MEM_TYPE) -@click.option('--flavor', '-f', - help="Public Virtual Server flavor key name", - type=click.STRING) -@click.option('--datacenter', '-d', - help="Datacenter shortname", - required=True, - prompt=True) -@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', - help="Specify the mode to boot the OS in. Supported modes are HVM and PV.", - type=click.STRING) -@click.option('--billing', - type=click.Choice(['hourly', 'monthly']), - default='hourly', - show_default=True, - help="Billing rate") -@click.option('--dedicated/--public', - is_flag=True, - help="Create a Dedicated Virtual Server") -@click.option('--host-id', - type=click.INT, - help="Host Id to provision a Dedicated Host Virtual Server onto") -@click.option('--san', - is_flag=True, - help="Use SAN storage instead of local disk.") -@click.option('--test', - is_flag=True, - help="Do not actually create the virtual server") -@click.option('--export', - type=click.Path(writable=True, resolve_path=True), +@click.option('--dedicated/--public', is_flag=True, help="Create a Dedicated Virtual Server") +@click.option('--host-id', type=click.INT, help="Host Id to provision a Dedicated Host Virtual Server onto") +@click.option('--san', is_flag=True, help="Use SAN storage instead of local disk.") +@click.option('--test', is_flag=True, help="Do not actually create the virtual server") +@click.option('--export', type=click.Path(writable=True, resolve_path=True), help="Exports options to a template file") @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('--key', '-k', help="SSH keys to add to the root user") @helpers.multi_option('--disk', help="Disk sizes") -@click.option('--private', - is_flag=True, +@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, +@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('--template', '-t', - is_eager=True, - callback=template.TemplateCallback(list_args=['disk', - 'key', - 'tag']), +@click.option('--template', '-t', is_eager=True, + callback=template.TemplateCallback(list_args=['disk', 'key', 'tag']), help="A template file that defaults the command-line options", type=click.Path(exists=True, readable=True, resolve_path=True)) @click.option('--userdata', '-u', help="User defined metadata string") +@click.option('--userfile', '-F', type=click.Path(exists=True, readable=True, resolve_path=True), + help="Read userdata from file") +@click.option('--vlan-public', type=click.INT, + help="The ID of the public VLAN on which you want the virtual server placed") +@click.option('--vlan-private', type=click.INT, + help="The ID of the private VLAN on which you want the virtual server placed") +@click.option('--subnet-public', type=click.INT, + help="The ID of the public SUBNET on which you want the virtual server placed") +@click.option('--subnet-private', type=click.INT, + help="The ID of the private SUBNET on which you want the virtual server placed") +@helpers.multi_option('--public-security-group', '-S', + help=('Security group ID to associate with the public interface')) +@helpers.multi_option('--private-security-group', '-s', + help=('Security group ID to associate with the private interface')) +@click.option('--wait', type=click.INT, + help="Wait until VS is finished provisioning for up to X seconds before returning") +@click.option('--placementgroup', + help="Placement Group name or Id to order this guest on. See: slcli vs placementgroup list") +@click.option('--ipv6', is_flag=True, help="Adds an IPv6 address to this guest") +@click.option('--transient', is_flag=True, + help="Create a transient virtual server") @click.option('--userfile', '-F', help="Read userdata from file", type=click.Path(exists=True, readable=True, resolve_path=True)) @@ -262,88 +240,63 @@ def _parse_create_args(client, args): @environment.pass_env def cli(env, **args): """Order/create virtual servers.""" + vsi = SoftLayer.VSManager(env.client) _validate_args(env, args) - - # Do not create a virtual server with test or export - do_create = not (args['export'] or args['test']) - - table = formatting.Table(['Item', 'cost']) - table.align['Item'] = 'r' - table.align['cost'] = 'r' config_list = _parse_create_args(env.client, args) - - output = [] - if args.get('test'): - for config in config_list: - result = vsi.verify_create_instance(**config) - 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]) - output.append(table) - output.append(formatting.FormattedItem( - None, - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guaranteed.')) - - if args['export']: - export_file = args.pop('export') - template.export_to_template(export_file, args, - exclude=['wait', 'test']) - env.fout('Successfully exported options to a template file.') + test = args.get('test', False) + do_create = not (args.get('export') or test) if do_create: if not (env.skip_confirmations or formatting.confirm( "This action will incur charges on your account. Continue?")): raise exceptions.CLIAbort('Aborting virtual server order.') - result = vsi.create_instances(config_list) - - if args.get('wait'): - ready = vsi.wait_for_ready(result['id'], args.get('wait') or 1) - table.add_row(['ready', ready]) - if ready is False: - env.out(env.fmt(output)) - raise exceptions.CLIHalt(code=1) + if args.get('export'): + export_file = args.pop('export') + template.export_to_template(export_file, args, exclude=['wait', 'test']) + env.fout('Successfully exported options to a template file.') + else: + result = vsi.create_instances(config_list) if args['output_json']: env.fout(json.dumps({'statuses': result})) else: - for instance_data in result: - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - table.add_row(['id', instance_data['id']]) - table.add_row(['hostname', instance_data['hostname']]) - table.add_row(['created', instance_data['createDate']]) - table.add_row(['uuid', instance_data['uuid']]) - output.append(table) + env.fout(result) + + +def _build_receipt_table(result, billing="hourly", test=False): + """Retrieve the total recurring fee of the items prices""" + title = "OrderId: %s" % (result.get('orderId', 'No order placed')) + table = formatting.Table(['Cost', 'Description'], title=title) + table.align['Cost'] = 'r' + table.align['Description'] = 'l' + total = 0.000 + if test: + prices = result['prices'] + else: + prices = result['orderDetails']['prices'] + + for item in prices: + rate = 0.000 + if billing == "hourly": + rate += float(item.get('hourlyRecurringFee', 0.000)) + else: + rate += float(item.get('recurringFee', 0.000)) + total += rate + table.add_row([rate, item['item']['description']]) + table.add_row(["%.3f" % total, "Total %s cost" % billing]) + return table + - env.fout(output) +def _build_guest_table(result): + table = formatting.Table(['ID', 'FQDN', 'guid', 'Order Date']) + table.align['name'] = 'r' + table.align['value'] = 'l' + virtual_guests = utils.lookup(result, 'orderDetails', 'virtualGuests') + for guest in virtual_guests: + table.add_row([guest['id'], guest['fullyQualifiedDomainName'], guest['globalIdentifier'], result['orderDate']]) + return table def _validate_args(env, args): @@ -357,6 +310,10 @@ def _validate_args(env, args): raise exceptions.ArgumentError( '[-m | --memory] not allowed with [-f | --flavor]') + if all([args['dedicated'], args['transient']]): + raise exceptions.ArgumentError( + '[--dedicated] not allowed with [--transient]') + if all([args['dedicated'], args['flavor']]): raise exceptions.ArgumentError( '[-d | --dedicated] not allowed with [-f | --flavor]') @@ -365,6 +322,10 @@ def _validate_args(env, args): raise exceptions.ArgumentError( '[-h | --host-id] not allowed with [-f | --flavor]') + if args['transient'] and args['billing'] == 'monthly': + raise exceptions.ArgumentError( + '[--transient] not allowed with [--billing monthly]') + if all([args['userdata'], args['userfile']]): raise exceptions.ArgumentError( '[-u | --userdata] not allowed with [-F | --userfile]') @@ -375,8 +336,6 @@ def _validate_args(env, args): '[-o | --os] not allowed with [--image]') while not any([args['os'], args['image']]): - args['os'] = env.input("Operating System Code", - default="", - show_default=False) + args['os'] = env.input("Operating System Code", default="", show_default=False) if not args['os']: args['image'] = env.input("Image", default="", show_default=False) diff --git a/SoftLayer/CLI/virt/create_options.py b/SoftLayer/CLI/virt/create_options.py index 7bacd8bf0..601c0f3ac 100644 --- a/SoftLayer/CLI/virt/create_options.py +++ b/SoftLayer/CLI/virt/create_options.py @@ -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 @@ -11,7 +12,7 @@ from SoftLayer import utils -@click.command() +@click.command(short_help="Get options to use for creating virtual servers.") @environment.pass_env def cli(env): """Virtual server order options.""" @@ -31,27 +32,7 @@ def cli(env): table.add_row(['datacenter', formatting.listing(datacenters, separator='\n')]) - def _add_flavor_rows(flavor_key, flavor_label, flavor_options): - flavors = [] - - for flavor_option in flavor_options: - flavor_key_name = utils.lookup(flavor_option, 'flavor', 'keyName') - if not flavor_key_name.startswith(flavor_key): - continue - - flavors.append(flavor_key_name) - - if len(flavors) > 0: - table.add_row(['flavors (%s)' % flavor_label, - formatting.listing(flavors, separator='\n')]) - - if result.get('flavors', None): - _add_flavor_rows('B1', 'balanced', result['flavors']) - _add_flavor_rows('BL1', 'balanced local - hdd', result['flavors']) - _add_flavor_rows('BL2', 'balanced local - ssd', result['flavors']) - _add_flavor_rows('C1', 'compute', result['flavors']) - _add_flavor_rows('M1', 'memory', result['flavors']) - _add_flavor_rows('AC', 'GPU', result['flavors']) + _add_flavors_to_table(result, table) # CPUs standard_cpus = [int(x['template']['startCpus']) for x in result['processors'] @@ -166,3 +147,36 @@ def add_block_rows(disks, name): formatting.listing(ded_host_speeds, separator=',')]) env.fout(table) + + +def _add_flavors_to_table(result, table): + grouping = { + 'balanced': {'key_starts_with': 'B1', 'flavors': []}, + 'balanced local - hdd': {'key_starts_with': 'BL1', 'flavors': []}, + 'balanced local - ssd': {'key_starts_with': 'BL2', 'flavors': []}, + 'compute': {'key_starts_with': 'C1', 'flavors': []}, + 'memory': {'key_starts_with': 'M1', 'flavors': []}, + 'GPU': {'key_starts_with': 'AC', 'flavors': []}, + 'transient': {'transient': True, 'flavors': []}, + } + + if result.get('flavors', None) is None: + return + + for flavor_option in result['flavors']: + flavor_key_name = utils.lookup(flavor_option, 'flavor', 'keyName') + + for name, group in grouping.items(): + if utils.lookup(flavor_option, 'template', 'transientGuestFlag') is True: + if utils.lookup(group, 'transient') is True: + group['flavors'].append(flavor_key_name) + break + + elif utils.lookup(group, 'key_starts_with') is not None \ + and flavor_key_name.startswith(group['key_starts_with']): + group['flavors'].append(flavor_key_name) + break + + for name, group in grouping.items(): + if len(group['flavors']) > 0: + table.add_row(['flavors (%s)' % name, formatting.listing(group['flavors'], separator='\n')]) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 4e2692c8c..83b49c445 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -63,65 +63,44 @@ def cli(env, identifier, passwords=False, price=False, output_json=False, table.add_row(['domain', result['domain']]) table.add_row(['fqdn', result['fullyQualifiedDomainName']]) table.add_row(['status', formatting.FormattedItem( - result['status']['keyName'] or formatting.blank(), - result['status']['name'] or formatting.blank() + result['status']['keyName'], + result['status']['name'] )]) table.add_row(['state', formatting.FormattedItem( utils.lookup(result, 'powerState', 'keyName'), utils.lookup(result, 'powerState', 'name'), )]) table.add_row(['active_transaction', formatting.active_txn(result)]) - table.add_row(['datacenter', - result['datacenter']['name'] or formatting.blank()]) + table.add_row(['datacenter', result['datacenter']['name'] or formatting.blank()]) _cli_helper_dedicated_host(env, result, table) operating_system = utils.lookup(result, 'operatingSystem', 'softwareLicense', 'softwareDescription') or {} - table.add_row(['os', operating_system.get('name') or formatting.blank()]) - table.add_row(['os_version', - operating_system.get('version') or formatting.blank()]) + table.add_row(['os', operating_system.get('name', '-')]) + table.add_row(['os_version', operating_system.get('version', '-')]) table.add_row(['cores', result['maxCpu']]) table.add_row(['memory', formatting.mb_to_gb(result['maxMemory'])]) - table.add_row(['public_ip', - result['primaryIpAddress'] or formatting.blank()]) - table.add_row(['private_ip', - result['primaryBackendIpAddress'] or formatting.blank()]) + table.add_row(['public_ip', result.get('primaryIpAddress', '-')]) + table.add_row(['private_ip', result.get('primaryBackendIpAddress', '-')]) table.add_row(['private_only', result['privateNetworkOnlyFlag']]) table.add_row(['private_cpu', result['dedicatedAccountHostOnlyFlag']]) + table.add_row(['transient', result.get('transientGuestFlag', False)]) table.add_row(['created', result['createDate']]) table.add_row(['modified', result['modifyDate']]) - if utils.lookup(result, 'billingItem') != []: - table.add_row(['owner', formatting.FormattedItem( - utils.lookup(result, 'billingItem', 'orderItem', - 'order', 'userRecord', - 'username') or formatting.blank(), - )]) - else: - table.add_row(['owner', formatting.blank()]) - vlan_table = formatting.Table(['type', 'number', 'id']) - for vlan in result['networkVlans']: - vlan_table.add_row([ - vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) - table.add_row(['vlans', vlan_table]) + table.add_row(_get_owner_row(result)) + table.add_row(_get_vlan_table(result)) - if result.get('networkComponents'): - secgroup_table = formatting.Table(['interface', 'id', 'name']) - has_secgroups = False - for comp in result.get('networkComponents'): - interface = 'PRIVATE' if comp['port'] == 0 else 'PUBLIC' - for binding in comp['securityGroupBindings']: - has_secgroups = True - secgroup = binding['securityGroup'] - secgroup_table.add_row([ - interface, secgroup['id'], - secgroup.get('name') or formatting.blank()]) - if has_secgroups: - table.add_row(['security_groups', secgroup_table]) + # Bug in v5.7.2 + # bandwidth = vsi.get_bandwidth_allocation(vs_id) + # table.add_row(['Bandwidth', _bw_table(bandwidth)]) + + security_table = _get_security_table(result) + if security_table is not None: + table.add_row(['security_groups', security_table]) - if result.get('notes'): - table.add_row(['notes', result['notes']]) + table.add_row(['notes', result.get('notes', '-')]) if price: total_price = utils.lookup(result, @@ -169,6 +148,20 @@ def cli(env, identifier, passwords=False, price=False, output_json=False, env.fout(table) +def _bw_table(bw_data): + """Generates a bandwidth useage table""" + table = formatting.Table(['Type', 'In GB', 'Out GB', 'Allotment']) + for bw_point in bw_data.get('useage'): + bw_type = 'Private' + allotment = 'N/A' + if bw_point['type']['alias'] == 'PUBLIC_SERVER_BW': + bw_type = 'Public' + allotment = bw_data['allotment'].get('amount', '-') + + table.add_row([bw_type, bw_point['amountIn'], bw_point['amountOut'], allotment]) + return table + + def _cli_helper_dedicated_host(env, result, table): """Get details on dedicated host for a virtual server.""" @@ -184,3 +177,40 @@ def _cli_helper_dedicated_host(env, result, table): dedicated_host = {} table.add_row(['dedicated_host', dedicated_host.get('name') or formatting.blank()]) + + +def _get_owner_row(result): + """Formats and resturns the Owner row""" + + if utils.lookup(result, 'billingItem') != []: + owner = utils.lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username') + else: + owner = formatting.blank() + return(['owner', owner]) + + +def _get_vlan_table(result): + """Formats and resturns a vlan table""" + + vlan_table = formatting.Table(['type', 'number', 'id']) + for vlan in result['networkVlans']: + vlan_table.add_row([ + vlan['networkSpace'], vlan['vlanNumber'], vlan['id']]) + return ['vlans', vlan_table] + + +def _get_security_table(result): + secgroup_table = formatting.Table(['interface', 'id', 'name']) + has_secgroups = False + + if result.get('networkComponents'): + for comp in result.get('networkComponents'): + interface = 'PRIVATE' if comp['port'] == 0 else 'PUBLIC' + for binding in comp['securityGroupBindings']: + has_secgroups = True + secgroup = binding['securityGroup'] + secgroup_table.add_row([interface, secgroup['id'], secgroup.get('name', '-')]) + if has_secgroups: + return secgroup_table + else: + return None diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 16d6ea033..88e89fff9 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -43,7 +43,7 @@ ] -@click.command() +@click.command(short_help="List virtual servers.") @click.option('--cpu', '-c', help='Number of CPU cores', type=click.INT) @click.option('--domain', '-D', help='Domain portion of the FQDN') @click.option('--datacenter', '-d', help='Datacenter shortname') @@ -52,6 +52,7 @@ @click.option('--network', '-n', help='Network port speed in Mbps') @click.option('--hourly', is_flag=True, help='Show only hourly instances') @click.option('--monthly', is_flag=True, help='Show only monthly instances') +@click.option('--transient', help='Filter by transient instances', type=click.BOOL) @helpers.multi_option('--tag', help='Filter by tags') @click.option('--sortby', help='Column to sort by', @@ -70,7 +71,7 @@ @click.option('--output-json', is_flag=True, default=False) @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tag, columns, limit, output_json): + hourly, monthly, tag, columns, limit, transient, output_json): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) @@ -82,6 +83,7 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, memory=memory, datacenter=datacenter, nic_speed=network, + transient=transient, tags=tag, mask=columns.mask(), limit=limit) diff --git a/SoftLayer/CLI/virt/placementgroup/__init__.py b/SoftLayer/CLI/virt/placementgroup/__init__.py new file mode 100644 index 000000000..aa748a5b1 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/__init__.py @@ -0,0 +1,46 @@ +"""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 PlacementGroupCommands(click.MultiCommand): + """Loads module for placement group related commands. + + Currently the base command loader only supports going two commands deep. + So this small loader is required for going that third level. + """ + + 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=PlacementGroupCommands, context_settings=CONTEXT) +def cli(): + """Base command for all capacity related concerns""" diff --git a/SoftLayer/CLI/virt/placementgroup/create.py b/SoftLayer/CLI/virt/placementgroup/create.py new file mode 100644 index 000000000..3051f9ac3 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/create.py @@ -0,0 +1,31 @@ +"""Create a placement group""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers +from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + + +@click.command() +@click.option('--name', type=click.STRING, required=True, prompt=True, help="Name for this new placement group.") +@click.option('--backend_router', '-b', required=True, prompt=True, + help="backendRouter, can be either the hostname or id.") +@click.option('--rule', '-r', required=True, prompt=True, + help="The keyName or Id of the rule to govern this placement group.") +@environment.pass_env +def cli(env, **args): + """Create a placement group.""" + manager = PlacementManager(env.client) + backend_router_id = helpers.resolve_id(manager.get_backend_router_id_from_hostname, + args.get('backend_router'), + 'backendRouter') + rule_id = helpers.resolve_id(manager.get_rule_id_from_name, args.get('rule'), 'Rule') + placement_object = { + 'name': args.get('name'), + 'backendRouterId': backend_router_id, + 'ruleId': rule_id + } + + result = manager.create(placement_object) + click.secho("Successfully created placement group: ID: %s, Name: %s" % (result['id'], result['name']), fg='green') diff --git a/SoftLayer/CLI/virt/placementgroup/create_options.py b/SoftLayer/CLI/virt/placementgroup/create_options.py new file mode 100644 index 000000000..790664cbc --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/create_options.py @@ -0,0 +1,38 @@ +"""List options for creating Placement Groups""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + + +@click.command() +@environment.pass_env +def cli(env): + """List options for creating a placement group.""" + manager = PlacementManager(env.client) + + routers = manager.get_routers() + env.fout(get_router_table(routers)) + + rules = manager.get_all_rules() + env.fout(get_rule_table(rules)) + + +def get_router_table(routers): + """Formats output from _get_routers and returns a table. """ + table = formatting.Table(['Datacenter', 'Hostname', 'Backend Router Id'], "Available Routers") + for router in routers: + datacenter = router['topLevelLocation']['longName'] + table.add_row([datacenter, router['hostname'], router['id']]) + return table + + +def get_rule_table(rules): + """Formats output from get_all_rules and returns a table. """ + table = formatting.Table(['Id', 'KeyName'], "Rules") + for rule in rules: + table.add_row([rule['id'], rule['keyName']]) + return table diff --git a/SoftLayer/CLI/virt/placementgroup/delete.py b/SoftLayer/CLI/virt/placementgroup/delete.py new file mode 100644 index 000000000..260b3cd35 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/delete.py @@ -0,0 +1,49 @@ +"""Delete a placement group.""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer.managers.vs import VSManager as VSManager +from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + + +@click.command(epilog="Once provisioned, virtual guests can be managed with the slcli vs commands") +@click.argument('identifier') +@click.option('--purge', is_flag=True, + help="Delete all guests in this placement group. " + "The group itself can be deleted once all VMs are fully reclaimed") +@environment.pass_env +def cli(env, identifier, purge): + """Delete a placement group. + + Placement Group MUST be empty before you can delete it. + + IDENTIFIER can be either the Name or Id of the placement group you want to view + """ + manager = PlacementManager(env.client) + group_id = helpers.resolve_id(manager.resolve_ids, identifier, 'placement_group') + + if purge: + placement_group = manager.get_object(group_id) + guest_list = ', '.join([guest['fullyQualifiedDomainName'] for guest in placement_group['guests']]) + if len(placement_group['guests']) < 1: + raise exceptions.CLIAbort('No virtual servers were found in placement group %s' % identifier) + + click.secho("You are about to delete the following guests!\n%s" % guest_list, fg='red') + if not (env.skip_confirmations or formatting.confirm("This action will cancel all guests! Continue?")): + raise exceptions.CLIAbort('Aborting virtual server order.') + vm_manager = VSManager(env.client) + for guest in placement_group['guests']: + click.secho("Deleting %s..." % guest['fullyQualifiedDomainName']) + vm_manager.cancel_instance(guest['id']) + return + + click.secho("You are about to delete the following placement group! %s" % identifier, fg='red') + if not (env.skip_confirmations or formatting.confirm("This action will cancel the placement group! Continue?")): + raise exceptions.CLIAbort('Aborting virtual server order.') + cancel_result = manager.delete(group_id) + if cancel_result: + click.secho("Placement Group %s has been canceld." % identifier, fg='green') diff --git a/SoftLayer/CLI/virt/placementgroup/detail.py b/SoftLayer/CLI/virt/placementgroup/detail.py new file mode 100644 index 000000000..9adf58932 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/detail.py @@ -0,0 +1,54 @@ +"""View details of a placement group""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + + +@click.command(epilog="Once provisioned, virtual guests can be managed with the slcli vs commands") +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """View details of a placement group. + + IDENTIFIER can be either the Name or Id of the placement group you want to view + """ + manager = PlacementManager(env.client) + group_id = helpers.resolve_id(manager.resolve_ids, identifier, 'placement_group') + result = manager.get_object(group_id) + table = formatting.Table(["Id", "Name", "Backend Router", "Rule", "Created"]) + + table.add_row([ + result['id'], + result['name'], + result['backendRouter']['hostname'], + result['rule']['name'], + result['createDate'] + ]) + guest_table = formatting.Table([ + "Id", + "FQDN", + "Primary IP", + "Backend IP", + "CPU", + "Memory", + "Provisioned", + "Transaction" + ]) + for guest in result['guests']: + guest_table.add_row([ + guest.get('id'), + guest.get('fullyQualifiedDomainName'), + guest.get('primaryIpAddress'), + guest.get('primaryBackendIpAddress'), + guest.get('maxCpu'), + guest.get('maxMemory'), + guest.get('provisionDate'), + formatting.active_txn(guest) + ]) + + env.fout(table) + env.fout(guest_table) diff --git a/SoftLayer/CLI/virt/placementgroup/list.py b/SoftLayer/CLI/virt/placementgroup/list.py new file mode 100644 index 000000000..94f72af1d --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/list.py @@ -0,0 +1,30 @@ +"""List Placement Groups""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + + +@click.command() +@environment.pass_env +def cli(env): + """List placement groups.""" + manager = PlacementManager(env.client) + result = manager.list() + table = formatting.Table( + ["Id", "Name", "Backend Router", "Rule", "Guests", "Created"], + title="Placement Groups" + ) + for group in result: + table.add_row([ + group['id'], + group['name'], + group['backendRouter']['hostname'], + group['rule']['name'], + group['guestCount'], + group['createDate'] + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/virt/upgrade.py b/SoftLayer/CLI/virt/upgrade.py index 419456f91..463fc077e 100644 --- a/SoftLayer/CLI/virt/upgrade.py +++ b/SoftLayer/CLI/virt/upgrade.py @@ -16,37 +16,30 @@ completed. However for Network, no reboot is required.""") @click.argument('identifier') @click.option('--cpu', type=click.INT, help="Number of CPU cores") -@click.option('--private', - is_flag=True, +@click.option('--private', is_flag=True, 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\nDo 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]): - raise exceptions.ArgumentError( - "Must provide [--cpu], [--memory], or [--network] to upgrade") + if not any([cpu, memory, network, flavor]): + raise exceptions.ArgumentError("Must provide [--cpu], [--memory], [--network], or [--flavor] to upgrade") if private and not cpu: - raise exceptions.ArgumentError( - "Must specify [--cpu] when using [--private]") + raise exceptions.ArgumentError("Must specify [--cpu] when using [--private]") vs_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS') - if not (env.skip_confirmations or formatting.confirm( - "This action will incur charges on your account. " - "Continue?")): + if not (env.skip_confirmations or formatting.confirm("This action will incur charges on your account. Continue?")): raise exceptions.CLIAbort('Aborted') if memory: memory = int(memory / 1024) - if not vsi.upgrade(vs_id, - cpus=cpu, - memory=memory, - nic_speed=network, - public=not private): + if not vsi.upgrade(vs_id, cpus=cpu, memory=memory, nic_speed=network, public=not private, preset=flavor): raise exceptions.CLIAbort('VS Upgrade Failed') diff --git a/SoftLayer/CLI/virt/usage.py b/SoftLayer/CLI/virt/usage.py new file mode 100644 index 000000000..9936d9416 --- /dev/null +++ b/SoftLayer/CLI/virt/usage.py @@ -0,0 +1,59 @@ +"""Usage information of a virtual server.""" +# :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 +from SoftLayer.utils import clean_time + + +@click.command() +@click.argument('identifier') +@click.option('--start_date', '-s', type=click.STRING, required=True, help="Start Date e.g. 2019-3-4 (yyyy-MM-dd)") +@click.option('--end_date', '-e', type=click.STRING, required=True, help="End Date e.g. 2019-4-2 (yyyy-MM-dd)") +@click.option('--valid_type', '-t', type=click.STRING, required=True, + help="Metric_Data_Type keyName e.g. CPU0, CPU1, MEMORY_USAGE, etc.") +@click.option('--summary_period', '-p', type=click.INT, default=3600, + help="300, 600, 1800, 3600, 43200 or 86400 seconds") +@environment.pass_env +def cli(env, identifier, start_date, end_date, valid_type, summary_period): + """Usage information of a virtual server.""" + + vsi = SoftLayer.VSManager(env.client) + table = formatting.Table(['counter', 'dateTime', 'type']) + table_average = formatting.Table(['Average']) + + vs_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS') + + result = vsi.get_summary_data_usage(vs_id, start_date=start_date, end_date=end_date, + valid_type=valid_type, summary_period=summary_period) + + if len(result) == 0: + raise exceptions.CLIAbort('No metric data for this range of dates provided') + + count = 0 + counter = 0.00 + for data in result: + if valid_type == "MEMORY_USAGE": + usage_counter = data['counter'] / 2 ** 30 + else: + usage_counter = data['counter'] + + table.add_row([ + round(usage_counter, 2), + clean_time(data['dateTime']), + data['type'], + ]) + counter = counter + usage_counter + count = count + 1 + + average = counter / count + + env.fout(table_average.add_row([round(average, 2)])) + + env.fout(table_average) + env.fout(table) diff --git a/SoftLayer/CLI/vpn/ipsec/subnet/add.py b/SoftLayer/CLI/vpn/ipsec/subnet/add.py index 438dfc5fc..08d0bc5ec 100644 --- a/SoftLayer/CLI/vpn/ipsec/subnet/add.py +++ b/SoftLayer/CLI/vpn/ipsec/subnet/add.py @@ -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 --git a/SoftLayer/CLI/vpn/ipsec/subnet/remove.py b/SoftLayer/CLI/vpn/ipsec/subnet/remove.py index 2d8b34d9b..41d450a33 100644 --- a/SoftLayer/CLI/vpn/ipsec/subnet/remove.py +++ b/SoftLayer/CLI/vpn/ipsec/subnet/remove.py @@ -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 --git a/SoftLayer/CLI/vpn/ipsec/translation/add.py b/SoftLayer/CLI/vpn/ipsec/translation/add.py index a0b7a35e6..952f7fd6c 100644 --- a/SoftLayer/CLI/vpn/ipsec/translation/add.py +++ b/SoftLayer/CLI/vpn/ipsec/translation/add.py @@ -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 --git a/SoftLayer/CLI/vpn/ipsec/translation/update.py b/SoftLayer/CLI/vpn/ipsec/translation/update.py index b78585db0..4f0709001 100644 --- a/SoftLayer/CLI/vpn/ipsec/translation/update.py +++ b/SoftLayer/CLI/vpn/ipsec/translation/update.py @@ -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 --git a/SoftLayer/CLI/vpn/ipsec/update.py b/SoftLayer/CLI/vpn/ipsec/update.py index 68e09b0a9..738a1d9f9 100644 --- a/SoftLayer/CLI/vpn/ipsec/update.py +++ b/SoftLayer/CLI/vpn/ipsec/update.py @@ -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 --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 3e79f6cd4..04ba36aaa 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -14,7 +14,8 @@ :license: MIT, see LICENSE for more details. """ -# pylint: disable=w0401,invalid-name +# pylint: disable=r0401,invalid-name,wildcard-import +# NOQA appears to no longer be working. The code might have been upgraded. from SoftLayer import consts from SoftLayer.API import * # NOQA diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 57a911e79..4046937e6 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -73,10 +73,17 @@ def __init__(self, username, api_key): def get_request(self, request): """Sets token-based auth headers.""" - request.headers['authenticate'] = { - 'username': self.username, - 'apiKey': self.api_key, - } + + # See https://cloud.ibm.com/docs/iam?topic=iam-iamapikeysforservices for why this is the way it is + if self.username == 'apikey': + request.transport_user = self.username + request.transport_password = self.api_key + else: + request.headers['authenticate'] = { + 'username': self.username, + 'apiKey': self.api_key, + } + return request def __repr__(self): diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 81bbe5be8..a9927d986 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v5.5.1' +VERSION = 'v5.7.2' 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 --git a/SoftLayer/decoration.py b/SoftLayer/decoration.py index 66ce62ccb..a5e35d740 100644 --- a/SoftLayer/decoration.py +++ b/SoftLayer/decoration.py @@ -15,7 +15,6 @@ exceptions.ServerError, exceptions.ApplicationError, exceptions.RemoteSystemError, - exceptions.TransportError ) diff --git a/SoftLayer/exceptions.py b/SoftLayer/exceptions.py index 5652730fa..b3530aa8c 100644 --- a/SoftLayer/exceptions.py +++ b/SoftLayer/exceptions.py @@ -57,34 +57,27 @@ class TransportError(SoftLayerAPIError): # XMLRPC Errors class NotWellFormed(ParseError): """Request was not well formed.""" - pass class UnsupportedEncoding(ParseError): """Encoding not supported.""" - pass class InvalidCharacter(ParseError): """There was an invalid character.""" - pass class SpecViolation(ServerError): """There was a spec violation.""" - pass class MethodNotFound(SoftLayerAPIError): """Method name not found.""" - pass class InvalidMethodParameters(SoftLayerAPIError): """Invalid method paramters.""" - pass class InternalError(ServerError): """Internal Server Error.""" - pass diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 586e597a9..cf884aefd 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -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'}, @@ -483,9 +489,38 @@ }] getActiveQuotes = [{ + 'accountId': 1234, 'id': 1234, 'name': 'TestQuote1234', 'quoteKey': '1234test4321', + 'createDate': '2019-04-10T14:26:03-06:00', + 'modifyDate': '2019-04-10T14:26:03-06:00', + 'order': { + 'id': 37623333, + 'items': [ + { + 'categoryCode': 'guest_core', + 'description': '4 x 2.0 GHz or higher Cores', + 'id': 468394713, + 'itemId': 859, + 'itemPriceId': '1642', + 'oneTimeAfterTaxAmount': '0', + 'oneTimeFee': '0', + 'oneTimeFeeTaxRate': '0', + 'oneTimeTaxAmount': '0', + 'quantity': 1, + 'recurringAfterTaxAmount': '0', + 'recurringFee': '0', + 'recurringTaxAmount': '0', + 'setupAfterTaxAmount': '0', + 'setupFee': '0', + 'setupFeeDeferralMonths': None, + 'setupFeeTaxRate': '0', + 'setupTaxAmount': '0', + 'package': {'id': 46, 'keyName': 'CLOUD_SERVER'} + }, + ] + } }] getOrders = [{ @@ -519,8 +554,8 @@ getNextInvoiceTotalAmount = 2 -getHubNetworkStorage = [{'id': 12345, 'username': 'SLOS12345-1'}, - {'id': 12346, 'username': 'SLOS12345-2'}] +getHubNetworkStorage = [{'id': 12345, 'username': 'SLOS12345-1', 'serviceResource': {'name': 'Cleversafe - US Region'}}, + {'id': 12346, 'username': 'SLOS12345-2', 'vendorName': 'Swift'}] getIscsiNetworkStorage = [{ 'accountId': 1234, @@ -553,11 +588,11 @@ 'name': 'dal05' }, 'memoryCapacity': 242, - 'name': 'khnguyendh', + 'name': 'test-dedicated', 'diskCapacity': 1200, 'guestCount': 1, 'cpuCount': 56, - 'id': 44701 + 'id': 12345 }] @@ -575,3 +610,108 @@ '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': 3, + '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' + } + }, + { + 'id': 3519 + } + ] + } +] + + +getPlacementGroups = [{ + "createDate": "2019-01-18T16:08:44-06:00", + "id": 12345, + "name": "test01", + "guestCount": 0, + "backendRouter": { + "hostname": "bcr01a.mex01", + "id": 329266 + }, + "rule": { + "id": 1, + "keyName": "SPREAD", + "name": "SPREAD" + } +}] + +getInvoices = [ + { + 'id': 33816665, + 'modifyDate': '2019-03-04T00:17:42-06:00', + 'createDate': '2019-03-04T00:17:42-06:00', + 'startingBalance': '129251.73', + 'statusCode': 'OPEN', + 'typeCode': 'RECURRING', + 'itemCount': 3317, + 'invoiceTotalAmount': '6230.66' + }, + { + 'id': 12345667, + 'modifyDate': '2019-03-05T00:17:42-06:00', + 'createDate': '2019-03-04T00:17:42-06:00', + 'startingBalance': '129251.73', + 'statusCode': 'OPEN', + 'typeCode': 'RECURRING', + 'itemCount': 12, + 'invoiceTotalAmount': '6230.66', + 'endingBalance': '12345.55' + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Billing_Invoice.py b/SoftLayer/fixtures/SoftLayer_Billing_Invoice.py new file mode 100644 index 000000000..d4d89131c --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Billing_Invoice.py @@ -0,0 +1,23 @@ +getInvoiceTopLevelItems = [ + { + 'categoryCode': 'sov_sec_ip_addresses_priv', + 'createDate': '2018-04-04T23:15:20-06:00', + 'description': '64 Portable Private IP Addresses', + 'id': 724951323, + 'oneTimeAfterTaxAmount': '0', + 'recurringAfterTaxAmount': '0', + 'hostName': 'bleg', + 'domainName': 'beh.com', + 'category': {'name': 'Private (only) Secondary VLAN IP Addresses'}, + 'children': [ + { + 'id': 12345, + 'category': {'name': 'Fake Child Category'}, + 'description': 'Blah', + 'oneTimeAfterTaxAmount': 55.50, + 'recurringAfterTaxAmount': 0.10 + } + ], + 'location': {'name': 'fra02'} + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py b/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py index 6302bfa94..f1ca8c497 100644 --- a/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py +++ b/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py @@ -3,16 +3,102 @@ 'id': 1234, 'name': 'TestQuote1234', 'quoteKey': '1234test4321', + 'order': { + 'id': 37623333, + 'items': [ + { + 'categoryCode': 'guest_core', + 'description': '4 x 2.0 GHz or higher Cores', + 'id': 468394713, + 'itemId': 859, + 'itemPriceId': '1642', + 'oneTimeAfterTaxAmount': '0', + 'oneTimeFee': '0', + 'oneTimeFeeTaxRate': '0', + 'oneTimeTaxAmount': '0', + 'quantity': 1, + 'recurringAfterTaxAmount': '0', + 'recurringFee': '0', + 'recurringTaxAmount': '0', + 'setupAfterTaxAmount': '0', + 'setupFee': '0', + 'setupFeeDeferralMonths': None, + 'setupFeeTaxRate': '0', + 'setupTaxAmount': '0', + 'package': {'id': 46, 'keyName': 'CLOUD_SERVER'} + }, + ] + } } getRecalculatedOrderContainer = { - 'orderContainers': [{ - 'presetId': '', + 'presetId': '', + 'prices': [{ + 'id': 1921 + }], + 'quantity': 1, + 'packageId': 50, + 'useHourlyPricing': '', + 'reservedCapacityId': '', + +} + +verifyOrder = { + 'orderId': 1234, + 'orderDate': '2013-08-01 15:23:45', + 'useHourlyPricing': False, + 'prices': [{ + 'id': 1, + 'laborFee': '2', + 'oneTimeFee': '2', + 'oneTimeFeeTax': '.1', + 'quantity': 1, + 'recurringFee': '2', + 'recurringFeeTax': '.1', + 'hourlyRecurringFee': '2', + 'setupFee': '1', + 'item': {'id': 1, 'description': 'this is a thing', 'keyName': 'TheThing'}, + }]} + +placeOrder = { + 'orderId': 1234, + 'orderDate': '2013-08-01 15:23:45', + 'orderDetails': { 'prices': [{ - 'id': 1921 + 'id': 1, + 'laborFee': '2', + 'oneTimeFee': '2', + 'oneTimeFeeTax': '.1', + 'quantity': 1, + 'recurringFee': '2', + 'recurringFeeTax': '.1', + 'hourlyRecurringFee': '2', + 'setupFee': '1', + 'item': {'id': 1, 'description': 'this is a thing'}, }], - 'quantity': 1, - 'packageId': 50, - 'useHourlyPricing': '', - }], + 'virtualGuests': [{ + 'id': 1234567, + 'globalIdentifier': '1a2b3c-1701', + 'fullyQualifiedDomainName': 'test.guest.com' + }], + }, + 'placedOrder': { + 'id': 37985543, + 'orderQuoteId': 2639077, + 'orderTypeId': 4, + 'status': 'PENDING_AUTO_APPROVAL', + 'items': [ + { + 'categoryCode': 'guest_core', + 'description': '4 x 2.0 GHz or higher Cores', + 'id': 472527133, + 'itemId': 859, + 'itemPriceId': '1642', + 'laborFee': '0', + 'oneTimeFee': '0', + 'recurringFee': '0', + 'setupFee': '0', + } + ] + } } diff --git a/SoftLayer/fixtures/SoftLayer_Event_Log.py b/SoftLayer/fixtures/SoftLayer_Event_Log.py index 8d9ce1e4a..840e84890 100644 --- a/SoftLayer/fixtures/SoftLayer_Event_Log.py +++ b/SoftLayer/fixtures/SoftLayer_Event_Log.py @@ -1,22 +1,164 @@ 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" + 'accountId': 100, + 'eventCreateDate': '2017-10-23T14:22:36.221541-05:00', + 'eventName': 'Disable Port', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '100', + 'userId': '', + 'userType': 'SYSTEM' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T09:40:41.830338-05:00', + 'eventName': 'Security Group Rule Added', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '{"securityGroupId":"200",' + '"securityGroupName":"test_SG",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public",' + '"requestId":"53d0b91d392864e062f4958",' + '"rules":[{"ruleId":"100",' + '"remoteIp":null,"remoteGroupId":null,"direction":"ingress",' + '"ethertype":"IPv4",' + '"portRangeMin":2000,"portRangeMax":2001,"protocol":"tcp"}]}', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '59e767e9c2184', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T09:40:32.238869-05:00', + 'eventName': 'Security Group Added', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '{"securityGroupId":"200",' + '"securityGroupName":"test_SG",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public",' + '"requestId":"96c9b47b9e102d2e1d81fba"}', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '59e767e03a57e', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:42:13.089536-05:00', + 'eventName': 'Security Group Rule(s) Removed', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"2abda7ca97e5a1444cae0b9",' + '"rules":[{"ruleId":"800",' + '"remoteIp":null,"remoteGroupId":null,"direction":"ingress",' + '"ethertype":"IPv4",' + '"portRangeMin":2000,"portRangeMax":2001,"protocol":"tcp"}]}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e7765515e28', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:42:11.679736-05:00', + 'eventName': 'Network Component Removed from Security Group', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"6b9a87a9ab8ac9a22e87a00",' + '"fullyQualifiedDomainName":"test.softlayer.com",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public"}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e77653a1e5f', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:41:49.802498-05:00', + 'eventName': 'Security Group Rule(s) Added', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"0a293c1c3e59e4471da6495",' + '"rules":[{"ruleId":"800",' + '"remoteIp":null,"remoteGroupId":null,"direction":"ingress",' + '"ethertype":"IPv4",' + '"portRangeMin":2000,"portRangeMax":2001,"protocol":"tcp"}]}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e7763dc3f1c', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:41:42.176328-05:00', + 'eventName': 'Network Component Added to Security Group', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"4709e02ad42c83f80345904",' + '"fullyQualifiedDomainName":"test.softlayer.com",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public"}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e77636261e7', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + } +] + +getAllEventObjectNames = [ + { + 'value': "Account" + }, + { + 'value': "CDN" + }, + { + 'value': "User" + }, + { + 'value': "Bare Metal Instance" + }, + { + 'value': "API Authentication" + }, + { + 'value': "Server" + }, + { + 'value': "CCI" + }, + { + 'value': "Image" + }, + { + 'value': "Bluemix LB" + }, + { + 'value': "Facility" + }, + { + 'value': "Cloud Object Storage" + }, + { + 'value': "Security Group" } ] diff --git a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py index 61bdbf984..47e9a1bcb 100644 --- a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py +++ b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py @@ -73,6 +73,7 @@ setTags = True setPrivateNetworkInterfaceSpeed = True setPublicNetworkInterfaceSpeed = True +toggleManagementInterface = True powerOff = True powerOn = True powerCycle = True @@ -80,6 +81,7 @@ rebootDefault = True rebootHard = True createFirmwareUpdateTransaction = True +createFirmwareReflashTransaction = True setUserMetadata = ['meta'] reloadOperatingSystem = 'OK' getReverseDomainRecords = [ @@ -116,3 +118,34 @@ } } ] + +getBandwidthAllotmentDetail = { + 'allocationId': 25465663, + 'bandwidthAllotmentId': 138442, + 'effectiveDate': '2019-04-03T23:00:00-06:00', + 'endEffectiveDate': None, + 'id': 25888247, + 'serviceProviderId': 1, + 'allocation': { + 'amount': '250' + } +} + +getBillingCycleBandwidthUsage = [ + { + 'amountIn': '.448', + 'amountOut': '.52157', + 'type': { + 'alias': 'PUBLIC_SERVER_BW' + } + }, + { + 'amountIn': '.03842', + 'amountOut': '.01822', + 'type': { + 'alias': 'PRIVATE_SERVER_BW' + } + } +] + +getMetricTrackingObjectId = 1000 diff --git a/SoftLayer/fixtures/SoftLayer_Metric_Tracking_Object.py b/SoftLayer/fixtures/SoftLayer_Metric_Tracking_Object.py new file mode 100644 index 000000000..50cfb197a --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Metric_Tracking_Object.py @@ -0,0 +1,57 @@ +getSummaryData = [ + { + "counter": 1.44, + "dateTime": "2019-03-04T00:00:00-06:00", + "type": "cpu0" + }, + { + "counter": 1.53, + "dateTime": "2019-03-04T00:05:00-06:00", + "type": "cpu0" + }, +] + + +# Using counter > 32bit int causes unit tests to fail. +getBandwidthData = [ + { + 'counter': 37.21, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'cpu0' + }, + { + 'counter': 76.12, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'cpu1' + }, + { + 'counter': 257623973, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'memory' + }, + { + 'counter': 137118503, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'memory_usage' + }, + { + 'counter': 125888818, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'privateIn_net_octet' + }, + { + 'counter': 961037, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'privateOut_net_octet' + }, + { + 'counter': 1449885176, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'publicIn_net_octet' + }, + { + 'counter': 91803794, + 'dateTime': '2019-05-20T23:00:00-06:00', + 'type': 'publicOut_net_octet' + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Cache_Purge.py b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Cache_Purge.py new file mode 100644 index 000000000..cd0d2810a --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Cache_Purge.py @@ -0,0 +1 @@ +createPurge = [] diff --git a/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Mapping.py b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Mapping.py new file mode 100644 index 000000000..51950b919 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Mapping.py @@ -0,0 +1,31 @@ +listDomainMappings = [ + { + "cname": "cdnakauuiet7s6u6.cdnedge.bluemix.net", + "domain": "test.example.com", + "header": "test.example.com", + "httpPort": 80, + "originHost": "1.1.1.1", + "originType": "HOST_SERVER", + "path": "/", + "protocol": "HTTP", + "status": "CNAME_CONFIGURATION", + "uniqueId": "9934111111111", + "vendorName": "akamai" + } +] + +listDomainMappingByUniqueId = [ + { + "cname": "cdnakauuiet7s6u6.cdnedge.bluemix.net", + "domain": "test.example.com", + "header": "test.example.com", + "httpPort": 80, + "originHost": "1.1.1.1", + "originType": "HOST_SERVER", + "path": "/", + "protocol": "HTTP", + "status": "CNAME_CONFIGURATION", + "uniqueId": "9934111111111", + "vendorName": "akamai" + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path.py b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path.py new file mode 100644 index 000000000..59705f7ba --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path.py @@ -0,0 +1,37 @@ +listOriginPath = [ + { + "header": "test.example.com", + "httpPort": 80, + "mappingUniqueId": "993419389425697", + "origin": "10.10.10.1", + "originType": "HOST_SERVER", + "path": "/example", + "status": "RUNNING" + }, + { + "header": "test.example.com", + "httpPort": 80, + "mappingUniqueId": "993419389425697", + "origin": "10.10.10.1", + "originType": "HOST_SERVER", + "path": "/example1", + "status": "RUNNING" + } +] + +createOriginPath = [ + { + "header": "test.example.com", + "httpPort": 80, + "mappingUniqueId": "993419389425697", + "origin": "10.10.10.1", + "originType": "HOST_SERVER", + "path": "/example", + "status": "RUNNING", + "bucketName": "test-bucket", + 'fileExtension': 'jpg', + "performanceConfiguration": "General web delivery" + } +] + +deleteOriginPath = "Origin with path /example/videos/* has been deleted" diff --git a/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Metrics.py b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Metrics.py new file mode 100644 index 000000000..6b6aab5b1 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_CdnMarketplace_Metrics.py @@ -0,0 +1,15 @@ +getMappingUsageMetrics = [ + { + "names": [ + "TotalBandwidth", + "TotalHits", + "HitRatio" + ], + "totals": [ + "0.0", + "0", + "0.0" + ], + "type": "TOTALS" + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py b/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py deleted file mode 100644 index 28e043bc8..000000000 --- a/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py +++ /dev/null @@ -1,37 +0,0 @@ -getObject = { - "cdnAccountName": "1234a", - "providerPortalAccessFlag": False, - "createDate": "2012-06-25T14:05:28-07:00", - "id": 1234, - "legacyCdnFlag": False, - "dependantServiceFlag": True, - "cdnSolutionName": "ORIGIN_PULL", - "statusId": 4, - "accountId": 1234, - "status": {'name': 'ACTIVE'}, -} - -getOriginPullMappingInformation = [ - { - "originUrl": "http://ams01.objectstorage.softlayer.net:80", - "mediaType": "FLASH", - "id": "12345", - "isSecureContent": False - }, - { - "originUrl": "http://sng01.objectstorage.softlayer.net:80", - "mediaType": "FLASH", - "id": "12345", - "isSecureContent": False - } -] - -createOriginPullMapping = True - -deleteOriginPullRule = True - -loadContent = True - -purgeContent = True - -purgeCache = True diff --git a/SoftLayer/fixtures/SoftLayer_Network_Pod.py b/SoftLayer/fixtures/SoftLayer_Network_Pod.py new file mode 100644 index 000000000..4e6088270 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_Pod.py @@ -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 --git a/SoftLayer/fixtures/SoftLayer_Network_SecurityGroup.py b/SoftLayer/fixtures/SoftLayer_Network_SecurityGroup.py index aa202ab9c..8d4b73283 100644 --- a/SoftLayer/fixtures/SoftLayer_Network_SecurityGroup.py +++ b/SoftLayer/fixtures/SoftLayer_Network_SecurityGroup.py @@ -39,8 +39,15 @@ 'createDate': '2017-05-05T12:44:43-06:00'} editObject = True deleteObject = True -addRules = True -editRules = True -removeRules = True -attachNetworkComponents = True -detachNetworkComponents = True +addRules = {"requestId": "addRules", + "rules": "[{'direction': 'ingress', " + "'portRangeMax': '', " + "'portRangeMin': '', " + "'ethertype': 'IPv4', " + "'securityGroupId': 100, " + "'remoteGroupId': '', " + "'id': 100}]"} +editRules = {'requestId': 'editRules'} +removeRules = {'requestId': 'removeRules'} +attachNetworkComponents = {'requestId': 'interfaceAdd'} +detachNetworkComponents = {'requestId': 'interfaceRemove'} diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage_Hub_Cleversafe_Account.py b/SoftLayer/fixtures/SoftLayer_Network_Storage_Hub_Cleversafe_Account.py new file mode 100644 index 000000000..4bc3f4fc7 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_Storage_Hub_Cleversafe_Account.py @@ -0,0 +1,39 @@ +credentialCreate = { + "accountId": "12345", + "createDate": "2019-04-05T13:25:25-06:00", + "id": 11111, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXMkAFhDOjHqYJva", + "username": "XfHhBNBPlPdlWyaPPJAI", + "type": { + "description": "A credential for generating S3 Compatible Signatures.", + "keyName": "S3_COMPATIBLE_SIGNATURE", + "name": "S3 Compatible Signature" + } +} + +getCredentials = [ + { + "accountId": "12345", + "createDate": "2019-04-05T13:25:25-06:00", + "id": 11111, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXMkAFhDOjHqYJva", + "username": "XfHhBNBPlPdlWyaPPJAI", + "type": { + "description": "A credential for generating S3 Compatible Signatures.", + "keyName": "S3_COMPATIBLE_SIGNATURE", + "name": "S3 Compatible Signature" + } + }, + { + "accountId": "12345", + "createDate": "2019-04-05T13:25:25-06:00", + "id": 11111, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXMkAFhDOjHqYJva", + "username": "XfHhBNBPlPdlWyaPPJAI", + "type": { + "description": "A credential for generating S3 Compatible Signatures.", + "keyName": "S3_COMPATIBLE_SIGNATURE", + "name": "S3 Compatible Signature" + } + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Notification_Occurrence_Event.py b/SoftLayer/fixtures/SoftLayer_Notification_Occurrence_Event.py new file mode 100644 index 000000000..7c6740431 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Notification_Occurrence_Event.py @@ -0,0 +1,31 @@ +getObject = { + 'endDate': '2019-03-18T17:00:00-06:00', + 'id': 1234, + 'lastImpactedUserCount': 417756, + 'modifyDate': '2019-03-12T15:32:48-06:00', + 'recoveryTime': None, + 'startDate': '2019-03-18T16:00:00-06:00', + 'subject': 'Public Website Maintenance', + 'summary': 'Blah Blah Blah', + 'systemTicketId': 76057381, + 'acknowledgedFlag': False, + 'attachments': [], + 'impactedResources': [{ + 'resourceType': 'Server', + 'resourceTableId': 12345, + 'hostname': 'test', + 'privateIp': '10.0.0.1', + 'filterLable': 'Server' + }], + 'notificationOccurrenceEventType': {'keyName': 'PLANNED'}, + 'statusCode': {'keyName': 'PUBLISHED', 'name': 'Published'}, + 'updates': [{ + 'contents': 'More Blah Blah', + 'createDate': '2019-03-12T13:07:22-06:00', + 'endDate': None, 'startDate': '2019-03-12T13:07:22-06:00' + }] +} + +getAllObjects = [getObject] + +acknowledgeNotification = True diff --git a/SoftLayer/fixtures/SoftLayer_Product_Order.py b/SoftLayer/fixtures/SoftLayer_Product_Order.py index 5b3cf27ca..3774f63a8 100644 --- a/SoftLayer/fixtures/SoftLayer_Product_Order.py +++ b/SoftLayer/fixtures/SoftLayer_Product_Order.py @@ -13,4 +13,94 @@ 'setupFee': '1', 'item': {'id': 1, 'description': 'this is a thing'}, }]} -placeOrder = verifyOrder +placeOrder = { + 'orderId': 1234, + 'orderDate': '2013-08-01 15:23:45', + 'orderDetails': { + 'prices': [{ + 'id': 1, + 'laborFee': '2', + 'oneTimeFee': '2', + 'oneTimeFeeTax': '.1', + 'quantity': 1, + 'recurringFee': '2', + 'recurringFeeTax': '.1', + 'hourlyRecurringFee': '2', + 'setupFee': '1', + 'item': {'id': 1, 'description': 'this is a thing'}, + }], + 'virtualGuests': [{ + 'id': 1234567, + 'globalIdentifier': '1a2b3c-1701', + 'fullyQualifiedDomainName': 'test.guest.com' + }] + } +} + +# 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', + }, + 'hourlyRecurringFee': 1.0, + 'recurringFee': 2.0 + } + ] + } +} diff --git a/SoftLayer/fixtures/SoftLayer_Product_Package.py b/SoftLayer/fixtures/SoftLayer_Product_Package.py index deef58258..186674282 100644 --- a/SoftLayer/fixtures/SoftLayer_Product_Package.py +++ b/SoftLayer/fixtures/SoftLayer_Product_Package.py @@ -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 --git a/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py b/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py new file mode 100644 index 000000000..ec3356c1d --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py @@ -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 --git a/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py b/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py index 9d1f99571..a7ecdb29a 100644 --- a/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py +++ b/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py @@ -6,3 +6,4 @@ 'notes': 'notes', 'key': 'ssh-rsa AAAAB3N...pa67 user@example.com'} createObject = getObject +getAllObjects = [getObject] diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py b/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py index ea1775eb9..43ab0ec34 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py @@ -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 --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py index 776db8778..270ecf2ad 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py @@ -14,6 +14,10 @@ {'nextInvoiceTotalRecurringAmount': 1}, {'nextInvoiceTotalRecurringAmount': 1}, ], + 'package': { + "id": 835, + "keyName": "PUBLIC_CLOUD_SERVER" + }, 'orderItem': { 'order': { 'userRecord': { @@ -45,6 +49,7 @@ 'vlanNumber': 23, 'id': 1}], 'dedicatedHost': {'id': 37401}, + 'transientGuestFlag': False, 'operatingSystem': { 'passwords': [{'username': 'user', 'password': 'pass'}], 'softwareLicense': { @@ -71,6 +76,17 @@ } } }, + { + 'flavor': { + 'keyName': 'B1_1X2X25_TRANSIENT' + }, + 'template': { + 'supplementalCreateObjectOptions': { + 'flavorKeyName': 'B1_1X2X25_TRANSIENT' + }, + 'transientGuestFlag': True + } + }, { 'flavor': { 'keyName': 'B1_1X2X100' @@ -419,11 +435,126 @@ 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 -createArchiveTransaction = {} +createArchiveTransaction = { + 'createDate': '2018-12-10T17:29:18-06:00', + 'elapsedSeconds': 0, + 'guestId': 12345678, + 'hardwareId': None, + 'id': 12345, + 'modifyDate': '2018-12-10T17:29:18-06:00', + 'statusChangeDate': '2018-12-10T17:29:18-06:00' +} + executeRescueLayer = True getUpgradeItemPrices = [ @@ -507,3 +638,35 @@ } }, ] + +getMetricTrackingObjectId = 1000 + + +getBandwidthAllotmentDetail = { + 'allocationId': 25465663, + 'bandwidthAllotmentId': 138442, + 'effectiveDate': '2019-04-03T23:00:00-06:00', + 'endEffectiveDate': None, + 'id': 25888247, + 'serviceProviderId': 1, + 'allocation': { + 'amount': '250' + } +} + +getBillingCycleBandwidthUsage = [ + { + 'amountIn': '.448', + 'amountOut': '.52157', + 'type': { + 'alias': 'PUBLIC_SERVER_BW' + } + }, + { + 'amountIn': '.03842', + 'amountOut': '.01822', + 'type': { + 'alias': 'PRIVATE_SERVER_BW' + } + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py index ee58ad762..785ed3b05 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py @@ -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 --git a/SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup.py b/SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup.py new file mode 100644 index 000000000..0159c6333 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup.py @@ -0,0 +1,63 @@ +getAvailableRouters = [{ + "accountId": 1, + "fullyQualifiedDomainName": "bcr01.dal01.softlayer.com", + "hostname": "bcr01.dal01", + "id": 1, + "topLevelLocation": { + "id": 3, + "longName": "Dallas 1", + "name": "dal01", + } +}] + +createObject = { + "accountId": 123, + "backendRouterId": 444, + "createDate": "2019-01-18T16:08:44-06:00", + "id": 5555, + "modifyDate": None, + "name": "test01", + "ruleId": 1 +} +getObject = { + "createDate": "2019-01-17T14:36:42-06:00", + "id": 1234, + "name": "test-group", + "backendRouter": { + "hostname": "bcr01a.mex01", + "id": 329266 + }, + "guests": [{ + "accountId": 123456789, + "createDate": "2019-01-17T16:44:46-06:00", + "domain": "test.com", + "fullyQualifiedDomainName": "issues10691547765077.test.com", + "hostname": "issues10691547765077", + "id": 69131875, + "maxCpu": 1, + "maxMemory": 1024, + "placementGroupId": 1234, + "provisionDate": "2019-01-17T16:47:17-06:00", + "activeTransaction": { + "id": 107585077, + "transactionStatus": { + "friendlyName": "TESTING TXN", + "name": "RECLAIM_WAIT" + } + }, + "globalIdentifier": "c786ac04-b612-4649-9d19-9662434eeaea", + "primaryBackendIpAddress": "10.131.11.14", + "primaryIpAddress": "169.57.70.180", + "status": { + "keyName": "DISCONNECTED", + "name": "Disconnected" + } + }], + "rule": { + "id": 1, + "keyName": "SPREAD", + "name": "SPREAD" + } +} + +deleteObject = True diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup_Rule.py b/SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup_Rule.py new file mode 100644 index 000000000..c933fd2db --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup_Rule.py @@ -0,0 +1,7 @@ +getAllObjects = [ + { + "id": 1, + "keyName": "SPREAD", + "name": "SPREAD" + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py b/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py new file mode 100644 index 000000000..67f496d6e --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py @@ -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 --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index f0602579e..5c489345d 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -11,13 +11,13 @@ from SoftLayer.managers.cdn import CDNManager from SoftLayer.managers.dedicated_host import DedicatedHostManager from SoftLayer.managers.dns import DNSManager +from SoftLayer.managers.event_log import EventLogManager from SoftLayer.managers.file import FileStorageManager from SoftLayer.managers.firewall import FirewallManager from SoftLayer.managers.hardware import HardwareManager from SoftLayer.managers.image import ImageManager from SoftLayer.managers.ipsec import IPSECManager from SoftLayer.managers.load_balancer import LoadBalancerManager -from SoftLayer.managers.messaging import MessagingManager from SoftLayer.managers.metadata import MetadataManager from SoftLayer.managers.network import NetworkManager from SoftLayer.managers.object_storage import ObjectStorageManager @@ -27,24 +27,27 @@ 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 +from SoftLayer.managers.vs_placement import PlacementManager __all__ = [ 'BlockStorageManager', + 'CapacityManager', 'CDNManager', 'DedicatedHostManager', 'DNSManager', + 'EventLogManager', 'FileStorageManager', 'FirewallManager', 'HardwareManager', 'ImageManager', 'IPSECManager', 'LoadBalancerManager', - 'MessagingManager', 'MetadataManager', 'NetworkManager', 'ObjectStorageManager', 'OrderingManager', + 'PlacementManager', 'SshKeyManager', 'SSLManager', 'TicketManager', diff --git a/SoftLayer/managers/account.py b/SoftLayer/managers/account.py new file mode 100644 index 000000000..1f7d4871d --- /dev/null +++ b/SoftLayer/managers/account.py @@ -0,0 +1,141 @@ +""" + SoftLayer.account + ~~~~~~~~~~~~~~~~~~~~~~~ + Account manager + + :license: MIT, see License for more details. +""" + +import logging + +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 AccountManager(utils.IdentifierMixin, object): + """Common functions for getting information from the Account service + + :param SoftLayer.API.BaseClient client: the client instance + """ + + def __init__(self, client): + self.client = client + + def get_summary(self): + """Gets some basic account information + + :return: Account object + """ + mask = """mask[ + nextInvoiceTotalAmount, + pendingInvoice[invoiceTotalAmount], + blockDeviceTemplateGroupCount, + dedicatedHostCount, + domainCount, + hardwareCount, + networkStorageCount, + openTicketCount, + networkVlanCount, + subnetCount, + userCount, + virtualGuestCount + ] + """ + return self.client.call('Account', 'getObject', mask=mask) + + def get_upcoming_events(self): + """Retreives a list of Notification_Occurrence_Events that have not ended yet + + :return: SoftLayer_Notification_Occurrence_Event + """ + mask = "mask[id, subject, startDate, endDate, statusCode, acknowledgedFlag, impactedResourceCount, updateCount]" + _filter = { + 'endDate': { + 'operation': '> sysdate' + }, + 'startDate': { + 'operation': 'orderBy', + 'options': [{ + 'name': 'sort', + 'value': ['ASC'] + }] + } + } + return self.client.call('Notification_Occurrence_Event', 'getAllObjects', filter=_filter, mask=mask, iter=True) + + def ack_event(self, event_id): + """Acknowledge an event. This mostly prevents it from appearing as a notification in the control portal. + + :param int event_id: Notification_Occurrence_Event ID you want to ack + :return: True on success, Exception otherwise. + """ + return self.client.call('Notification_Occurrence_Event', 'acknowledgeNotification', id=event_id) + + def get_event(self, event_id): + """Gets details about a maintenance event + + :param int event_id: Notification_Occurrence_Event ID + :return: Notification_Occurrence_Event + """ + mask = """mask[ + acknowledgedFlag, + attachments, + impactedResources, + statusCode, + updates, + notificationOccurrenceEventType] + """ + return self.client.call('Notification_Occurrence_Event', 'getObject', id=event_id, mask=mask) + + def get_invoices(self, limit=50, closed=False, get_all=False): + """Gets an accounts invoices. + + :param int limit: Number of invoices to get back in a single call. + :param bool closed: If True, will also get CLOSED invoices + :param bool get_all: If True, will paginate through invoices until all have been retrieved. + :return: Billing_Invoice + """ + mask = "mask[invoiceTotalAmount, itemCount]" + _filter = { + 'invoices': { + 'createDate': { + 'operation': 'orderBy', + 'options': [{ + 'name': 'sort', + 'value': ['DESC'] + }] + }, + 'statusCode': {'operation': 'OPEN'}, + } + } + if closed: + del _filter['invoices']['statusCode'] + + return self.client.call('Account', 'getInvoices', mask=mask, filter=_filter, iter=get_all, limit=limit) + + def get_billing_items(self, identifier): + """Gets all topLevelBillingItems from a specific invoice + + :param int identifier: Invoice Id + :return: Billing_Invoice_Item + """ + + mask = """mask[ + id, description, hostName, domainName, oneTimeAfterTaxAmount, recurringAfterTaxAmount, createDate, + categoryCode, + category[name], + location[name], + children[id, category[name], description, oneTimeAfterTaxAmount, recurringAfterTaxAmount] + ]""" + return self.client.call( + 'Billing_Invoice', + 'getInvoiceTopLevelItems', + id=identifier, + mask=mask, + iter=True, + limit=100 + ) diff --git a/SoftLayer/managers/cdn.py b/SoftLayer/managers/cdn.py index 994ab8fab..51dfb5252 100644 --- a/SoftLayer/managers/cdn.py +++ b/SoftLayer/managers/cdn.py @@ -5,141 +5,156 @@ :license: MIT, see LICENSE for more details. """ -import six from SoftLayer import utils -MAX_URLS_PER_LOAD = 5 -MAX_URLS_PER_PURGE = 5 - - class CDNManager(utils.IdentifierMixin, object): - """Manage CDN accounts and content. + """Manage Content Delivery Networks in the account. See product information here: - http://www.softlayer.com/content-delivery-network + https://www.ibm.com/cloud/cdn + https://cloud.ibm.com/docs/infrastructure/CDN?topic=CDN-about-content-delivery-networks-cdn- :param SoftLayer.API.BaseClient client: the client instance """ def __init__(self, client): self.client = client - self.account = self.client['Network_ContentDelivery_Account'] - - def list_accounts(self): - """Lists CDN accounts for the active user.""" - - account = self.client['Account'] - mask = 'cdnAccounts[%s]' % ', '.join(['id', - 'createDate', - 'cdnAccountName', - 'cdnSolutionName', - 'cdnAccountNote', - 'status']) - return account.getObject(mask=mask).get('cdnAccounts', []) - - def get_account(self, account_id, **kwargs): - """Retrieves a CDN account with the specified account ID. - - :param account_id int: the numeric ID associated with the CDN account. - :param dict \\*\\*kwargs: additional arguments to include in the object - mask. - """ + self.cdn_configuration = self.client['Network_CdnMarketplace_Configuration_Mapping'] + self.cdn_path = self.client['SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path'] + self.cdn_metrics = self.client['Network_CdnMarketplace_Metrics'] + self.cdn_purge = self.client['SoftLayer_Network_CdnMarketplace_Configuration_Cache_Purge'] - if 'mask' not in kwargs: - kwargs['mask'] = 'status' + def list_cdn(self, **kwargs): + """Lists Content Delivery Networks for the active user. - return self.account.getObject(id=account_id, **kwargs) + :param dict \\*\\*kwargs: header-level options (mask, limit, etc.) + :returns: The list of CDN objects in the account + """ - def get_origins(self, account_id, **kwargs): - """Retrieves list of origin pull mappings for a specified CDN account. + return self.cdn_configuration.listDomainMappings(**kwargs) - :param account_id int: the numeric ID associated with the CDN account. - :param dict \\*\\*kwargs: additional arguments to include in the object - mask. - """ + def get_cdn(self, unique_id, **kwargs): + """Retrieves the information about the CDN account object. - return self.account.getOriginPullMappingInformation(id=account_id, - **kwargs) - - def add_origin(self, account_id, media_type, origin_url, cname=None, - secure=False): - """Adds an original pull mapping to an origin-pull. - - :param int account_id: the numeric ID associated with the CDN account. - :param string media_type: the media type/protocol associated with this - origin pull mapping; valid values are HTTP, - FLASH, and WM. - :param string origin_url: the base URL from which content should be - pulled. - :param string cname: an optional CNAME that should be associated with - this origin pull rule; only the hostname should be - included (i.e., no 'http://', directories, etc.). - :param boolean secure: specifies whether this is an SSL origin pull - rule, if SSL is enabled on your account - (defaults to false). + :param str unique_id: The unique ID associated with the CDN. + :param dict \\*\\*kwargs: header-level option (mask) + :returns: The CDN object """ - config = {'mediaType': media_type, - 'originUrl': origin_url, - 'isSecureContent': secure} + cdn_list = self.cdn_configuration.listDomainMappingByUniqueId(unique_id, **kwargs) - if cname: - config['cname'] = cname + # The method listDomainMappingByUniqueId() returns an array but there is only 1 object + return cdn_list[0] - return self.account.createOriginPullMapping(config, id=account_id) + def get_origins(self, unique_id, **kwargs): + """Retrieves list of origin pull mappings for a specified CDN account. + + :param str unique_id: The unique ID associated with the CDN. + :param dict \\*\\*kwargs: header-level options (mask, limit, etc.) + :returns: The list of origin paths in the CDN object. + """ - def remove_origin(self, account_id, origin_id): + return self.cdn_path.listOriginPath(unique_id, **kwargs) + + def add_origin(self, unique_id, origin, path, origin_type="server", header=None, + port=80, protocol='http', bucket_name=None, file_extensions=None, + optimize_for="web", cache_query="include all"): + """Creates an origin path for an existing CDN. + + :param str unique_id: The unique ID associated with the CDN. + :param str path: relative path to the domain provided, e.g. "/articles/video" + :param str origin: ip address or hostname if origin_type=server, API endpoint for + your S3 object storage if origin_type=storage + :param str origin_type: it can be 'server' or 'storage' types. + :param str header: the edge server uses the host header to communicate with the origin. + It defaults to hostname. (optional) + :param int port: the http port number (default: 80) + :param str protocol: the protocol of the origin (default: HTTP) + :param str bucket_name: name of the available resource + :param str file_extensions: file extensions that can be stored in the CDN, e.g. "jpg,png" + :param str optimize_for: performance configuration, available options: web, video, and file + where: + 'web' --> 'General web delivery' + 'video' --> 'Video on demand optimization' + 'file' --> 'Large file optimization' + :param str cache_query: rules with the following formats: 'include-all', 'ignore-all', + 'include: space separated query-names', + 'ignore: space separated query-names'.' + :return: a CDN origin path object + """ + types = {'server': 'HOST_SERVER', 'storage': 'OBJECT_STORAGE'} + performance_config = { + 'web': 'General web delivery', + 'video': 'Video on demand optimization', + 'file': 'Large file optimization' + } + + new_origin = { + 'uniqueId': unique_id, + 'path': path, + 'origin': origin, + 'originType': types.get(origin_type), + 'httpPort': port, + 'protocol': protocol.upper(), + 'performanceConfiguration': performance_config.get(optimize_for, 'General web delivery'), + 'cacheKeyQueryRule': cache_query + } + + if header: + new_origin['header'] = header + + if types.get(origin_type) == 'OBJECT_STORAGE': + if bucket_name: + new_origin['bucketName'] = bucket_name + + if file_extensions: + new_origin['fileExtension'] = file_extensions + + origin = self.cdn_path.createOriginPath(new_origin) + + # The method createOriginPath() returns an array but there is only 1 object + return origin[0] + + def remove_origin(self, unique_id, path): """Removes an origin pull mapping with the given origin pull ID. - :param int account_id: the CDN account ID from which the mapping should - be deleted. - :param int origin_id: the origin pull mapping ID to delete. + :param str unique_id: The unique ID associated with the CDN. + :param str path: The origin path to delete. + :returns: A string value """ - return self.account.deleteOriginPullRule(origin_id, id=account_id) + return self.cdn_path.deleteOriginPath(unique_id, path) - def load_content(self, account_id, urls): - """Prefetches one or more URLs to the CDN edge nodes. + def purge_content(self, unique_id, path): + """Purges a URL or path from the CDN. - :param int account_id: the CDN account ID into which content should be - preloaded. - :param urls: a string or a list of strings representing the CDN URLs - that should be pre-loaded. - :returns: true if all load requests were successfully submitted; - otherwise, returns the first error encountered. + :param str unique_id: The unique ID associated with the CDN. + :param str path: A string of url or path that should be purged. + :returns: A Container_Network_CdnMarketplace_Configuration_Cache_Purge array object """ - if isinstance(urls, six.string_types): - urls = [urls] - - for i in range(0, len(urls), MAX_URLS_PER_LOAD): - result = self.account.loadContent(urls[i:i + MAX_URLS_PER_LOAD], - id=account_id) - if not result: - return result + return self.cdn_purge.createPurge(unique_id, path) - return True + def get_usage_metrics(self, unique_id, history=30, frequency="aggregate"): + """Retrieves the cdn usage metrics. - def purge_content(self, account_id, urls): - """Purges one or more URLs from the CDN edge nodes. + It uses the 'days' argument if start_date and end_date are None. - :param int account_id: the CDN account ID from which content should - be purged. - :param urls: a string or a list of strings representing the CDN URLs - that should be purged. - :returns: true if all purge requests were successfully submitted; - otherwise, returns the first error encountered. + :param int unique_id: The CDN uniqueId from which the usage metrics will be obtained. + :param int history: Last N days, default days is 30. + :param str frequency: It can be day, week, month and aggregate. The default is "aggregate". + :returns: A Container_Network_CdnMarketplace_Metrics object """ - if isinstance(urls, six.string_types): - urls = [urls] + _start = utils.days_to_datetime(history) + _end = utils.days_to_datetime(0) + + _start_date = utils.timestamp(_start) + _end_date = utils.timestamp(_end) - for i in range(0, len(urls), MAX_URLS_PER_PURGE): - result = self.account.purgeCache(urls[i:i + MAX_URLS_PER_PURGE], - id=account_id) - if not result: - return result + usage = self.cdn_metrics.getMappingUsageMetrics(unique_id, _start_date, _end_date, frequency) - return True + # The method getMappingUsageMetrics() returns an array but there is only 1 object + return usage[0] diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index b5e36613b..db8c6d8e1 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -33,46 +33,144 @@ def __init__(self, client, ordering_manager=None): 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, reason='unneeded', comment='', immediate=True): - """Cancels the specified dedicated server. + def cancel_host(self, host_id): + """Cancel a dedicated host immediately, it fails if there are still guests in the host. - Example:: + :param host_id: The ID of the dedicated host to be cancelled. + :return: True on success or an exception - # Cancels dedicated host id 1234 - result = mgr.cancel_host(host_id=1234) + Example:: + # Cancels dedicated host id 12345 + result = mgr.cancel_host(12345) - :param int host_id: The ID of the dedicated host to be cancelled. - :param string reason: The reason code for the cancellation. This should come from - :func:`get_cancellation_reasons`. - :param string comment: An optional comment to include with the cancellation. - :param bool immediate: If set to True, will automatically update the cancelation ticket to request - the resource be reclaimed asap. This request still has to be reviewed by a human - :returns: True on success or an exception """ + return self.host.deleteObject(id=host_id) - # Get cancel reason - reasons = self.get_cancellation_reasons() - cancel_reason = reasons.get(reason, reasons['unneeded']) - ticket_mgr = SoftLayer.TicketManager(self.client) - mask = 'mask[id, billingItem[id]]' - host_billing = self.get_host(host_id, mask=mask) + 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() - if 'billingItem' not in host_billing: - raise SoftLayer.SoftLayerError("Ticket #%s already exists for this server" % - host_billing['openCancellationTicket']['id']) + :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. - billing_id = host_billing['billingItem']['id'] + Example:: + # Cancel guests of dedicated host id 12345 + result = mgr.cancel_guests(12345) + """ + result = [] - result = self.client.call('Billing_Item', 'cancelItem', - immediate, False, cancel_reason, comment, id=billing_id) + 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 edit(self, host_id, userdata=None, hostname=None, domain=None, notes=None, tags=None): """Edit hostname, domain name, notes, user data of the dedicated host. @@ -355,7 +453,8 @@ def _get_package(self): capacity, keyName, itemCategory[categoryCode], - bundleItems[capacity, categories[categoryCode]] + bundleItems[capacity,keyName,categories[categoryCode],hardwareGenericComponentModel[id, + hardwareComponentType[keyName]]] ], regions[location[location[priceGroups]]] ''' @@ -429,6 +528,32 @@ def _get_backend_router(self, locations, item): 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: @@ -441,6 +566,8 @@ def _get_backend_router(self, locations, item): 'id': loc_id } } + if item['keyName'].find("GPU") != -1: + host['pciDevices'] = gpuComponents routers = self.host.getAvailableRouters(host, mask=mask) return routers @@ -468,6 +595,15 @@ def get_router_options(self, datacenter=None, flavor=None): 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 + # @retry(logger=LOGGER) def set_tags(self, tags, host_id): """Sets tags on a dedicated_host with a retry decorator diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index c1b7b3b60..a3fc322af 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -89,17 +89,81 @@ def create_record(self, zone_id, record, record_type, data, ttl=60): :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 --git a/SoftLayer/managers/event_log.py b/SoftLayer/managers/event_log.py new file mode 100644 index 000000000..cc0a7f5cd --- /dev/null +++ b/SoftLayer/managers/event_log.py @@ -0,0 +1,91 @@ +""" + SoftLayer.event_log + ~~~~~~~~~~~~~~~~~~~ + Network Manager/helpers + + :license: MIT, see LICENSE for more details. +""" + +from SoftLayer import utils + + +class EventLogManager(object): + """Provides an interface for the SoftLayer Event Log Service. + + See product information here: + http://sldn.softlayer.com/reference/services/SoftLayer_Event_Log + """ + + def __init__(self, client): + self.client = client + self.event_log = client['Event_Log'] + + def get_event_logs(self, request_filter=None, log_limit=20, iterator=True): + """Returns a list of event logs + + Example:: + + event_mgr = SoftLayer.EventLogManager(env.client) + request_filter = event_mgr.build_filter(date_min="01/01/2019", date_max="02/01/2019") + logs = event_mgr.get_event_logs(request_filter) + for log in logs: + print("Event Name: {}".format(log['eventName'])) + + + :param dict request_filter: filter dict + :param int log_limit: number of results to get in one API call + :param bool iterator: False will only make one API call for log_limit results. + True will keep making API calls until all logs have been retreived. There may be a lot of these. + :returns: List of event logs. If iterator=True, will return a python generator object instead. + """ + if iterator: + # Call iter_call directly as this returns the actual generator + return self.client.iter_call('Event_Log', 'getAllObjects', filter=request_filter, limit=log_limit) + return self.client.call('Event_Log', 'getAllObjects', filter=request_filter, limit=log_limit) + + def get_event_log_types(self): + """Returns a list of event log types + + :returns: List of event log types + """ + results = self.event_log.getAllEventObjectNames() + return results + + @staticmethod + def build_filter(date_min=None, date_max=None, obj_event=None, obj_id=None, obj_type=None, utc_offset=None): + """Returns a query filter that can be passed into EventLogManager.get_event_logs + + :param string date_min: Lower bound date in MM/DD/YYYY format + :param string date_max: Upper bound date in MM/DD/YYYY format + :param string obj_event: The name of the events we want to filter by + :param int obj_id: The id of the event we want to filter by + :param string obj_type: The type of event we want to filter by + :param string utc_offset: The UTC offset we want to use when converting date_min and date_max. + (default '+0000') + + :returns: dict: The generated query filter + """ + + if not any([date_min, date_max, obj_event, obj_id, obj_type]): + return {} + + request_filter = {} + + if date_min and date_max: + request_filter['eventCreateDate'] = utils.event_log_filter_between_date(date_min, date_max, utc_offset) + else: + if date_min: + request_filter['eventCreateDate'] = utils.event_log_filter_greater_than_date(date_min, utc_offset) + elif date_max: + request_filter['eventCreateDate'] = utils.event_log_filter_less_than_date(date_max, utc_offset) + + if obj_event: + request_filter['eventName'] = {'operation': obj_event} + + if obj_id: + request_filter['objectId'] = {'operation': obj_id} + + if obj_type: + request_filter['objectName'] = {'operation': obj_type} + + return request_filter diff --git a/SoftLayer/managers/file.py b/SoftLayer/managers/file.py index e0a250328..b6d16053f 100644 --- a/SoftLayer/managers/file.py +++ b/SoftLayer/managers/file.py @@ -482,6 +482,9 @@ def cancel_file_volume(self, volume_id, reason='No longer needed', immediate=Fal file_volume = self.get_file_volume_details( volume_id, mask='mask[id,billingItem[id,hourlyFlag]]') + + if 'billingItem' not in file_volume: + raise exceptions.SoftLayerError('The volume has already been canceled') billing_item_id = file_volume['billingItem']['id'] if utils.lookup(file_volume, 'billingItem', 'hourlyFlag'): diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index ae4d3d296..a1337c202 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -92,7 +92,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', immediate= 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', @@ -268,7 +268,7 @@ def reload(self, hardware_id, post_uri=None, ssh_keys=None): """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 """ @@ -658,6 +658,31 @@ def update_firmware(self, return self.hardware.createFirmwareUpdateTransaction( bool(ipmi), bool(raid_controller), bool(bios), bool(hard_drive), id=hardware_id) + def reflash_firmware(self, + hardware_id, + ipmi=True, + raid_controller=True, + bios=True): + """Reflash hardware firmware. + + This will cause the server to be unavailable for ~60 minutes. + The firmware will not be upgraded but rather reflashed to the version installed. + + :param int hardware_id: The ID of the hardware to have its firmware + reflashed. + :param bool ipmi: Reflash the ipmi firmware. + :param bool raid_controller: Reflash the raid controller firmware. + :param bool bios: Reflash the bios firmware. + + Example:: + + # Check the servers active transactions to see progress + result = mgr.reflash_firmware(hardware_id=1234) + """ + + return self.hardware.createFirmwareReflashTransaction( + bool(ipmi), bool(raid_controller), bool(bios), id=hardware_id) + def wait_for_ready(self, instance_id, limit=14400, delay=10, pending=False): """Determine if a Server is ready. @@ -684,6 +709,40 @@ def wait_for_ready(self, instance_id, limit=14400, delay=10, pending=False): LOGGER.info("Waiting for %d expired.", instance_id) return False + def get_tracking_id(self, instance_id): + """Returns the Metric Tracking Object Id for a hardware server + + :param int instance_id: Id of the hardware server + """ + return self.hardware.getMetricTrackingObjectId(id=instance_id) + + def get_bandwidth_data(self, instance_id, start_date=None, end_date=None, direction=None, rollup=3600): + """Gets bandwidth data for a server + + Will get averaged bandwidth data for a given time period. If you use a rollup over 3600 be aware + that the API will bump your start/end date to align with how data is stored. For example if you + have a rollup of 86400 your start_date will be bumped to 00:00. If you are not using a time in the + start/end date fields, this won't really matter. + + :param int instance_id: Hardware Id to get data for + :param date start_date: Date to start pulling data for. + :param date end_date: Date to finish pulling data for + :param string direction: Can be either 'public', 'private', or None for both. + :param int rollup: 300, 600, 1800, 3600, 43200 or 86400 seconds to average data over. + """ + tracking_id = self.get_tracking_id(instance_id) + data = self.client.call('Metric_Tracking_Object', 'getBandwidthData', start_date, end_date, direction, + rollup, id=tracking_id, iter=True) + return data + + def get_bandwidth_allocation(self, instance_id): + """Combines getBandwidthAllotmentDetail() and getBillingCycleBandwidthUsage() """ + a_mask = "mask[allocation[amount]]" + allotment = self.client.call('Hardware_Server', 'getBandwidthAllotmentDetail', id=instance_id, mask=a_mask) + u_mask = "mask[amountIn,amountOut,type]" + useage = self.client.call('Hardware_Server', 'getBillingCycleBandwidthUsage', id=instance_id, mask=u_mask) + return {'allotment': allotment['allocation'], 'useage': useage} + def _get_extra_price_id(items, key_name, hourly, location): """Returns a price id attached to item with the given key_name.""" diff --git a/SoftLayer/managers/image.py b/SoftLayer/managers/image.py index 81eee9282..34efb36bf 100644 --- a/SoftLayer/managers/image.py +++ b/SoftLayer/managers/image.py @@ -16,7 +16,7 @@ class ImageManager(utils.IdentifierMixin, object): """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,70 @@ def edit(self, image_id, name=None, note=None, tag=None): 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_crn=None, + wrapped_dek=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 + :param string ibm_api_key: Ibm Api Key needed to communicate with ICOS + and your KMS + :param string root_key_crn: CRN of the root key in your KMS. Go to your + KMS (Key Protect or Hyper Protect) provider to get the CRN for your + root key. An example CRN: + crn:v1:bluemix:public:hs-crypto:us-south:acctID:serviceID:key:keyID' + Used only when is_encrypted is True. + :param string wrapped_dek: Wrapped Data Encryption Key provided by + your KMS. Used only when is_encrypted is True. + :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 """ - return self.vgbdtg.createFromExternalSource({ - 'name': name, - 'note': note, - 'operatingSystemReferenceCode': os_code, - 'uri': uri, - }) - - def export_image_to_uri(self, image_id, uri): + if 'cos://' in uri: + return self.vgbdtg.createFromIcos({ + 'name': name, + 'note': note, + 'operatingSystemReferenceCode': os_code, + 'uri': uri, + 'ibmApiKey': ibm_api_key, + 'crkCrn': root_key_crn, + 'wrappedDek': wrapped_dek, + '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, 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://@// + or cos://// if using IBM Cloud + Object Storage + :param string ibm_api_key: Ibm Api Key needed to communicate with IBM + Cloud Object Storage """ - return self.vgbdtg.copyToExternalSource({'uri': uri}, id=image_id) + 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 --git a/SoftLayer/managers/ipsec.py b/SoftLayer/managers/ipsec.py index 130623c48..29317516e 100644 --- a/SoftLayer/managers/ipsec.py +++ b/SoftLayer/managers/ipsec.py @@ -227,10 +227,6 @@ def update_translation(self, context_id, translation_id, static_ip=None, 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 --git a/SoftLayer/managers/messaging.py b/SoftLayer/managers/messaging.py deleted file mode 100644 index 3ce1c17aa..000000000 --- a/SoftLayer/managers/messaging.py +++ /dev/null @@ -1,405 +0,0 @@ -""" - SoftLayer.messaging - ~~~~~~~~~~~~~~~~~~~ - Manager for the SoftLayer Message Queue service - - :license: MIT, see LICENSE for more details. -""" -import json - -import requests.auth - -from SoftLayer import consts -from SoftLayer import exceptions -# pylint: disable=no-self-use - - -ENDPOINTS = { - "dal05": { - "public": "dal05.mq.softlayer.net", - "private": "dal05.mq.service.networklayer.com" - } -} - - -class QueueAuth(requests.auth.AuthBase): - """SoftLayer Message Queue authentication for requests. - - :param endpoint: endpoint URL - :param username: SoftLayer username - :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 - self.api_key = api_key - self.auth_token = auth_token - - def auth(self): - """Authenticate.""" - headers = { - 'X-Auth-User': self.username, - 'X-Auth-Key': self.api_key - } - resp = requests.post(self.endpoint, headers=headers) - if resp.ok: - self.auth_token = resp.headers['X-Auth-Token'] - else: - raise exceptions.Unauthenticated("Error while authenticating: %s" - % resp.status_code) - - def handle_error(self, resp, **_): - """Handle errors.""" - resp.request.deregister_hook('response', self.handle_error) - if resp.status_code == 503: - resp.connection.send(resp.request) - elif resp.status_code == 401: - self.auth() - resp.request.headers['X-Auth-Token'] = self.auth_token - resp.connection.send(resp.request) - - def __call__(self, resp): - """Attach auth token to the request. - - Do authentication if an auth token isn't available - """ - if not self.auth_token: - self.auth() - resp.register_hook('response', self.handle_error) - resp.headers['X-Auth-Token'] = self.auth_token - return resp - - -class MessagingManager(object): - """Manage SoftLayer Message Queue accounts. - - See product information here: http://www.softlayer.com/message-queue - - :param SoftLayer.API.BaseClient client: the client instance - - """ - - def __init__(self, client): - self.client = client - - def list_accounts(self, **kwargs): - """List message queue accounts. - - :param dict \\*\\*kwargs: response-level options (mask, limit, etc.) - """ - if 'mask' not in kwargs: - items = [ - 'id', - 'name', - 'status', - 'nodes', - ] - kwargs['mask'] = "mask[%s]" % ','.join(items) - - return self.client['Account'].getMessageQueueAccounts(**kwargs) - - def get_endpoint(self, datacenter=None, network=None): - """Get a message queue endpoint based on datacenter/network type. - - :param datacenter: datacenter code - :param network: network ('public' or 'private') - """ - if datacenter is None: - datacenter = 'dal05' - if network is None: - network = 'public' - try: - host = ENDPOINTS[datacenter][network] - return "https://%s" % host - except KeyError: - raise TypeError('Invalid endpoint %s/%s' - % (datacenter, network)) - - def get_endpoints(self): - """Get all known message queue endpoints.""" - return ENDPOINTS - - def get_connection(self, account_id, datacenter=None, network=None): - """Get connection to Message Queue Service. - - :param account_id: Message Queue Account id - :param datacenter: Datacenter code - :param network: network ('public' or 'private') - """ - if any([not self.client.auth, - not getattr(self.client.auth, 'username', None), - not getattr(self.client.auth, 'api_key', None)]): - raise exceptions.SoftLayerError( - 'Client instance auth must be BasicAuthentication.') - - client = MessagingConnection( - account_id, endpoint=self.get_endpoint(datacenter, network)) - client.authenticate(self.client.auth.username, - self.client.auth.api_key) - return client - - def ping(self, datacenter=None, network=None): - """Ping a message queue endpoint.""" - resp = requests.get('%s/v1/ping' % - self.get_endpoint(datacenter, network)) - resp.raise_for_status() - return True - - -class MessagingConnection(object): - """Message Queue Service Connection. - - :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 - self.auth = None - - def _make_request(self, method, path, **kwargs): - """Make request. Generally not called directly. - - :param method: HTTP Method - :param path: resource Path - :param dict \\*\\*kwargs: extra request arguments - """ - headers = { - 'Content-Type': 'application/json', - 'User-Agent': consts.USER_AGENT, - } - headers.update(kwargs.get('headers', {})) - kwargs['headers'] = headers - kwargs['auth'] = self.auth - - url = '/'.join((self.endpoint, 'v1', self.account_id, path)) - resp = requests.request(method, url, **kwargs) - try: - resp.raise_for_status() - except requests.HTTPError as ex: - content = json.loads(ex.response.content) - raise exceptions.SoftLayerAPIError(ex.response.status_code, - content['message']) - return resp - - def authenticate(self, username, api_key, auth_token=None): - """Authenticate this connection using the given credentials. - - :param username: SoftLayer username - :param api_key: SoftLayer API Key - :param auth_token: (optional) Starting auth token - """ - auth_endpoint = '/'.join((self.endpoint, 'v1', - self.account_id, 'auth')) - auth = QueueAuth(auth_endpoint, username, api_key, - auth_token=auth_token) - auth.auth() - self.auth = auth - - def stats(self, period='hour'): - """Get account stats. - - :param period: 'hour', 'day', 'week', 'month' - """ - resp = self._make_request('get', 'stats/%s' % period) - return resp.json() - - # QUEUE METHODS - - def get_queues(self, tags=None): - """Get listing of queues. - - :param list tags: (optional) list of tags to filter by - """ - params = {} - if tags: - params['tags'] = ','.join(tags) - resp = self._make_request('get', 'queues', params=params) - return resp.json() - - def create_queue(self, queue_name, **kwargs): - """Create Queue. - - :param queue_name: Queue Name - :param dict \\*\\*kwargs: queue options - """ - queue = {} - queue.update(kwargs) - data = json.dumps(queue) - resp = self._make_request('put', 'queues/%s' % queue_name, data=data) - return resp.json() - - def modify_queue(self, queue_name, **kwargs): - """Modify Queue. - - :param queue_name: Queue Name - :param dict \\*\\*kwargs: queue options - """ - return self.create_queue(queue_name, **kwargs) - - def get_queue(self, queue_name): - """Get queue details. - - :param queue_name: Queue Name - """ - resp = self._make_request('get', 'queues/%s' % queue_name) - return resp.json() - - def delete_queue(self, queue_name, force=False): - """Delete Queue. - - :param queue_name: Queue Name - :param force: (optional) Force queue to be deleted even if there - are pending messages - """ - params = {} - if force: - params['force'] = 1 - self._make_request('delete', 'queues/%s' % queue_name, params=params) - return True - - def push_queue_message(self, queue_name, body, **kwargs): - """Create Queue Message. - - :param queue_name: Queue Name - :param body: Message body - :param dict \\*\\*kwargs: Message options - """ - message = {'body': body} - message.update(kwargs) - resp = self._make_request('post', 'queues/%s/messages' % queue_name, - data=json.dumps(message)) - return resp.json() - - def pop_messages(self, queue_name, count=1): - """Pop messages from a queue. - - :param queue_name: Queue Name - :param count: (optional) number of messages to retrieve - """ - resp = self._make_request('get', 'queues/%s/messages' % queue_name, - params={'batch': count}) - return resp.json() - - def pop_message(self, queue_name): - """Pop a single message from a queue. - - If no messages are returned this returns None - - :param queue_name: Queue Name - """ - messages = self.pop_messages(queue_name, count=1) - if messages['item_count'] > 0: - return messages['items'][0] - else: - return None - - def delete_message(self, queue_name, message_id): - """Delete a message. - - :param queue_name: Queue Name - :param message_id: Message id - """ - self._make_request('delete', 'queues/%s/messages/%s' - % (queue_name, message_id)) - return True - - # TOPIC METHODS - - def get_topics(self, tags=None): - """Get listing of topics. - - :param list tags: (optional) list of tags to filter by - """ - params = {} - if tags: - params['tags'] = ','.join(tags) - resp = self._make_request('get', 'topics', params=params) - return resp.json() - - def create_topic(self, topic_name, **kwargs): - """Create Topic. - - :param topic_name: Topic Name - :param dict \\*\\*kwargs: Topic options - """ - data = json.dumps(kwargs) - resp = self._make_request('put', 'topics/%s' % topic_name, data=data) - return resp.json() - - def modify_topic(self, topic_name, **kwargs): - """Modify Topic. - - :param topic_name: Topic Name - :param dict \\*\\*kwargs: Topic options - """ - return self.create_topic(topic_name, **kwargs) - - def get_topic(self, topic_name): - """Get topic details. - - :param topic_name: Topic Name - """ - resp = self._make_request('get', 'topics/%s' % topic_name) - return resp.json() - - def delete_topic(self, topic_name, force=False): - """Delete Topic. - - :param topic_name: Topic Name - :param force: (optional) Force topic to be deleted even if there - are attached subscribers - """ - params = {} - if force: - params['force'] = 1 - self._make_request('delete', 'topics/%s' % topic_name, params=params) - return True - - def push_topic_message(self, topic_name, body, **kwargs): - """Create Topic Message. - - :param topic_name: Topic Name - :param body: Message body - :param dict \\*\\*kwargs: Topic message options - """ - message = {'body': body} - message.update(kwargs) - resp = self._make_request('post', 'topics/%s/messages' % topic_name, - data=json.dumps(message)) - return resp.json() - - def get_subscriptions(self, topic_name): - """Listing of subscriptions on a topic. - - :param topic_name: Topic Name - """ - resp = self._make_request('get', - 'topics/%s/subscriptions' % topic_name) - return resp.json() - - def create_subscription(self, topic_name, subscription_type, **kwargs): - """Create Subscription. - - :param topic_name: Topic Name - :param subscription_type: type ('queue' or 'http') - :param dict \\*\\*kwargs: Subscription options - """ - resp = self._make_request( - 'post', 'topics/%s/subscriptions' % topic_name, - data=json.dumps({ - 'endpoint_type': subscription_type, 'endpoint': kwargs})) - return resp.json() - - def delete_subscription(self, topic_name, subscription_id): - """Delete a subscription. - - :param topic_name: Topic Name - :param subscription_id: Subscription id - """ - self._make_request('delete', 'topics/%s/subscriptions/%s' % - (topic_name, subscription_id)) - return True diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index b568e8896..dbfb9c3f6 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -6,10 +6,13 @@ :license: MIT, see LICENSE for more details. """ import collections +import json from SoftLayer import exceptions from SoftLayer import utils +from SoftLayer.managers import event_log + DEFAULT_SUBNET_MASK = ','.join(['hardware', 'datacenter', 'ipAddressCount', @@ -107,13 +110,13 @@ def add_securitygroup_rules(self, group_id, rules): raise TypeError("The rules provided must be a list of dictionaries") return self.security_group.addRules(rules, id=group_id) - def add_subnet(self, subnet_type, quantity=None, vlan_id=None, version=4, + def add_subnet(self, subnet_type, quantity=None, endpoint_id=None, version=4, test_order=False): """Orders a new subnet - :param str subnet_type: Type of subnet to add: private, public, global + :param str subnet_type: Type of subnet to add: private, public, global,static :param int quantity: Number of IPs in the subnet - :param int vlan_id: VLAN id for the subnet to be placed into + :param int endpoint_id: id for the subnet to be placed into :param int version: 4 for IPv4, 6 for IPv6 :param bool test_order: If true, this will only verify the order. """ @@ -123,9 +126,11 @@ def add_subnet(self, subnet_type, quantity=None, vlan_id=None, version=4, if version == 4: if subnet_type == 'global': quantity = 0 - category = 'global_ipv4' + category = "global_ipv4" elif subnet_type == 'public': - category = 'sov_sec_ip_addresses_pub' + category = "sov_sec_ip_addresses_pub" + elif subnet_type == 'static': + category = "static_sec_ip_addresses" else: category = 'static_ipv6_addresses' if subnet_type == 'global': @@ -134,6 +139,8 @@ def add_subnet(self, subnet_type, quantity=None, vlan_id=None, version=4, desc = 'Global' elif subnet_type == 'public': desc = 'Portable' + elif subnet_type == 'static': + desc = 'Static' # In the API, every non-server item is contained within package ID 0. # This means that we need to get all of the items and loop through them @@ -141,7 +148,8 @@ def add_subnet(self, subnet_type, quantity=None, vlan_id=None, version=4, # item description. price_id = None quantity_str = str(quantity) - for item in package.getItems(id=0, mask='itemCategory'): + package_items = package.getItems(id=0) + for item in package_items: category_code = utils.lookup(item, 'itemCategory', 'categoryCode') if all([category_code == category, item.get('capacity') == quantity_str, @@ -150,10 +158,6 @@ def add_subnet(self, subnet_type, quantity=None, vlan_id=None, version=4, price_id = item['prices'][0]['id'] break - if not price_id: - raise TypeError('Invalid combination specified for ordering a' - ' subnet.') - order = { 'packageId': 0, 'prices': [{'id': price_id}], @@ -162,9 +166,10 @@ def add_subnet(self, subnet_type, quantity=None, vlan_id=None, version=4, # correct order container 'complexType': 'SoftLayer_Container_Product_Order_Network_Subnet', } - - if subnet_type != 'global': - order['endPointVlanId'] = vlan_id + if subnet_type == 'static': + order['endPointIpAddressId'] = endpoint_id + elif subnet_type != 'global' and subnet_type != 'static': + order['endPointVlanId'] = endpoint_id if test_order: return self.client['Product_Order'].verifyOrder(order) @@ -372,7 +377,7 @@ def get_securitygroup(self, group_id, **kwargs): 'description,' '''rules[id, remoteIp, remoteGroupId, direction, ethertype, portRangeMin, - portRangeMax, protocol],''' + portRangeMax, protocol, createDate, modifyDate],''' '''networkComponentBindings[ networkComponent[ id, @@ -544,6 +549,45 @@ def remove_securitygroup_rules(self, group_id, rules): """ return self.security_group.removeRules(rules, id=group_id) + def get_event_logs_by_request_id(self, request_id): + """Gets all event logs by the given request id + + :param string request_id: The request id we want to filter on + """ + + # Get all relevant event logs + unfiltered_logs = self._get_cci_event_logs() + self._get_security_group_event_logs() + + # Grab only those that have the specific request id + filtered_logs = [] + + for unfiltered_log in unfiltered_logs: + try: + metadata = json.loads(unfiltered_log['metaData']) + if 'requestId' in metadata: + if metadata['requestId'] == request_id: + filtered_logs.append(unfiltered_log) + except ValueError: + continue + + return filtered_logs + + def _get_cci_event_logs(self): + # Load the event log manager + event_log_mgr = event_log.EventLogManager(self.client) + + # Get CCI Event Logs + _filter = event_log_mgr.build_filter(obj_type='CCI') + return event_log_mgr.get_event_logs(request_filter=_filter) + + def _get_security_group_event_logs(self): + # Load the event log manager + event_log_mgr = event_log.EventLogManager(self.client) + + # Get CCI Event Logs + _filter = event_log_mgr.build_filter(obj_type='Security Group') + return event_log_mgr.get_event_logs(request_filter=_filter) + def resolve_global_ip_ids(self, identifier): """Resolve global ip ids.""" return utils.resolve_ids(identifier, diff --git a/SoftLayer/managers/object_storage.py b/SoftLayer/managers/object_storage.py index 393d16db7..2560d26c8 100644 --- a/SoftLayer/managers/object_storage.py +++ b/SoftLayer/managers/object_storage.py @@ -6,8 +6,8 @@ :license: MIT, see LICENSE for more details. """ -LIST_ACCOUNTS_MASK = '''mask(SoftLayer_Network_Storage_Hub_Swift)[ - id,username,notes +LIST_ACCOUNTS_MASK = '''mask[ + id,username,notes,vendorName,serviceResource ]''' ENDPOINT_MASK = '''mask(SoftLayer_Network_Storage_Hub_Swift)[ @@ -29,12 +29,8 @@ def __init__(self, client): def list_accounts(self): """Lists your object storage accounts.""" - _filter = { - 'hubNetworkStorage': {'vendorName': {'operation': 'Swift'}}, - } return self.client.call('Account', 'getHubNetworkStorage', - mask=LIST_ACCOUNTS_MASK, - filter=_filter) + mask=LIST_ACCOUNTS_MASK) def list_endpoints(self): """Lists the known object storage endpoints.""" @@ -56,3 +52,47 @@ def list_endpoints(self): }) return endpoints + + def create_credential(self, identifier): + """Create object storage credential. + + :param int identifier: The object storage account identifier. + + """ + + return self.client.call('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'credentialCreate', + id=identifier) + + def delete_credential(self, identifier, credential_id=None): + """Delete the object storage credential. + + :param int id: The object storage account identifier. + :param int credential_id: The credential id to be deleted. + + """ + credential = { + 'id': credential_id + } + + return self.client.call('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'credentialDelete', + credential, id=identifier) + + def limit_credential(self, identifier): + """Limit object storage credentials. + + :param int identifier: The object storage account identifier. + + """ + + return self.client.call('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'getCredentialLimit', + id=identifier) + + def list_credential(self, identifier): + """List the object storage credentials. + + :param int identifier: The object storage account identifier. + + """ + + return self.client.call('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'getCredentials', + id=identifier) diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 2847c6242..f38cb34de 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -11,12 +11,13 @@ from SoftLayer import exceptions + CATEGORY_MASK = '''id, isRequired, itemCategory[id, name, categoryCode] ''' -ITEM_MASK = '''id, keyName, description, itemCategory, categories''' +ITEM_MASK = '''id, keyName, description, itemCategory, categories, prices''' PACKAGE_MASK = '''id, name, keyName, isActive, type''' @@ -34,6 +35,7 @@ def __init__(self, client): 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. @@ -131,12 +133,12 @@ def get_package_id_by_type(self, package_type): raise ValueError("No package found for type: " + package_type) def get_quotes(self): - """Retrieve a list of quotes. + """Retrieve a list of active quotes. :returns: a list of SoftLayer_Billing_Order_Quote """ - - quotes = self.client['Account'].getActiveQuotes() + mask = "mask[order[id,items[id,package[id,keyName]]]]" + quotes = self.client['Account'].getActiveQuotes(mask=mask) return quotes def get_quote_details(self, quote_id): @@ -145,7 +147,8 @@ def get_quote_details(self, quote_id): :param quote_id: ID number of target quote """ - quote = self.client['Billing_Order_Quote'].getObject(id=quote_id) + mask = "mask[order[id,items[package[id,keyName]]]]" + quote = self.client['Billing_Order_Quote'].getObject(id=quote_id, mask=mask) return quote def get_order_container(self, quote_id): @@ -156,62 +159,75 @@ def get_order_container(self, quote_id): quote = self.client['Billing_Order_Quote'] container = quote.getRecalculatedOrderContainer(id=quote_id) - return container['orderContainers'][0] + return container def generate_order_template(self, quote_id, extra, quantity=1): """Generate a complete order template. :param int quote_id: ID of target quote - :param list extra: List of dictionaries that have extra details about - the order such as hostname or domain names for - virtual servers or hardware nodes - :param int quantity: Number of ~things~ to order + :param dictionary extra: Overrides for the defaults of SoftLayer_Container_Product_Order + :param int quantity: Number of items to order. """ - container = self.get_order_container(quote_id) - container['quantity'] = quantity + if not isinstance(extra, dict): + raise ValueError("extra is not formatted properly") - # NOTE(kmcdonald): This will only work with virtualGuests and hardware. - # There has to be a better way, since this is based on - # an existing quote that supposedly knows about this - # detail - if container['packageId'] == 46: - product_type = 'virtualGuests' - else: - product_type = 'hardware' + container = self.get_order_container(quote_id) - if len(extra) != quantity: - raise ValueError("You must specify extra for each server in the quote") + container['quantity'] = quantity + for key in extra.keys(): + container[key] = extra[key] - container[product_type] = [] - for extra_details in extra: - container[product_type].append(extra_details) - container['presetId'] = None return container - def verify_quote(self, quote_id, extra, quantity=1): + def verify_quote(self, quote_id, extra): """Verifies that a quote order is valid. + :: + + extras = { + 'hardware': {'hostname': 'test', 'domain': 'testing.com'}, + 'quantity': 2 + } + manager = ordering.OrderingManager(env.client) + result = manager.verify_quote(12345, extras) + + :param int quote_id: ID for the target quote - :param list hostnames: hostnames of the servers - :param string domain: domain of the new servers + :param dictionary extra: Overrides for the defaults of SoftLayer_Container_Product_Order :param int quantity: Quantity to override default """ + container = self.generate_order_template(quote_id, extra) + clean_container = {} + + # There are a few fields that wil cause exceptions in the XML endpoing if you send in '' + # reservedCapacityId and hostId specifically. But we clean all just to be safe. + # This for some reason is only a problem on verify_quote. + for key in container.keys(): + if container.get(key) != '': + clean_container[key] = container[key] - container = self.generate_order_template(quote_id, extra, quantity=quantity) - return self.order_svc.verifyOrder(container) + return self.client.call('SoftLayer_Billing_Order_Quote', 'verifyOrder', clean_container, id=quote_id) - def order_quote(self, quote_id, extra, quantity=1): + def order_quote(self, quote_id, extra): """Places an order using a quote + :: + + extras = { + 'hardware': {'hostname': 'test', 'domain': 'testing.com'}, + 'quantity': 2 + } + manager = ordering.OrderingManager(env.client) + result = manager.order_quote(12345, extras) + :param int quote_id: ID for the target quote - :param list hostnames: hostnames of the servers - :param string domain: domain of the new server + :param dictionary extra: Overrides for the defaults of SoftLayer_Container_Product_Order :param int quantity: Quantity to override default """ - container = self.generate_order_template(quote_id, extra, quantity=quantity) - return self.order_svc.placeOrder(container) + container = self.generate_order_template(quote_id, extra) + return self.client.call('SoftLayer_Billing_Order_Quote', 'placeOrder', container, id=quote_id) def get_package_by_key(self, package_keyname, mask=None): """Get a single package with a given key. @@ -321,7 +337,7 @@ def get_preset_by_key(self, package_keyname, preset_keyname, mask=None): 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,6 +346,7 @@ def get_price_id_list(self, package_keyname, item_keynames): :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 @@ -338,7 +355,8 @@ def get_price_id_list(self, package_keyname, item_keynames): items = self.list_items(package_keyname, mask=mask) prices = [] - gpu_number = -1 + category_dict = {"gpu0": -1, "pcie_slot0": -1} + for item_keyname in item_keynames: try: # Need to find the item in the package that has a matching @@ -354,21 +372,66 @@ def get_price_id_list(self, package_keyname, item_keynames): # 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 - if matching_item['itemCategory']['categoryCode'] != "gpu0": - price_id = [p['id'] for p in matching_item['prices'] - if not p['locationGroupId']][0] + item_category = matching_item['itemCategory']['categoryCode'] + if item_category not in category_dict: + 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 + # GPU and PCIe items has two generic prices and they are added to the list + # according to the number of items in the order. + category_dict[item_category] += 1 + category_code = item_category[:-1] + str(category_dict[item_category]) price_id = [p['id'] for p in matching_item['prices'] if not p['locationGroupId'] - and p['categories'][0]['categoryCode'] == "gpu" + str(gpu_number)][0] + and p['categories'][0]['categoryCode'] == category_code][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. @@ -430,6 +493,39 @@ def place_order(self, package_keyname, location, item_keynames, complex_type=Non 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. @@ -467,19 +563,24 @@ def generate_order(self, package_keyname, location, item_keynames, complex_type= # 'domain': 'softlayer.com'}]} order.update(extras) order['packageId'] = package['id'] - order['location'] = self.get_location_id(location) order['quantity'] = quantity + order['location'] = self.get_location_id(location) 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] container['orderContainers'] = [order] diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index f0d75dc37..3fa2ac1dd 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -61,7 +61,7 @@ def add_certificate(self, certificate): :param dict certificate: A dictionary representing the parts of the certificate. - See developer.softlayer.com for more info. + See sldn.softlayer.com for more info. Example:: diff --git a/SoftLayer/managers/storage_utils.py b/SoftLayer/managers/storage_utils.py index 07f19bd73..7cce7671b 100644 --- a/SoftLayer/managers/storage_utils.py +++ b/SoftLayer/managers/storage_utils.py @@ -241,7 +241,7 @@ def find_saas_endurance_space_price(package, size, tier_level): key_name = 'STORAGE_SPACE_FOR_{0}_IOPS_PER_GB'.format(tier_level) key_name = key_name.replace(".", "_") for item in package['items']: - if item['keyName'] != key_name: + if key_name not in item['keyName']: continue if 'capacityMinimum' not in item or 'capacityMaximum' not in item: diff --git a/SoftLayer/managers/ticket.py b/SoftLayer/managers/ticket.py index 0155f0d5f..04f8470b0 100644 --- a/SoftLayer/managers/ticket.py +++ b/SoftLayer/managers/ticket.py @@ -40,7 +40,7 @@ def list_tickets(self, open_status=True, closed_status=True): 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.""" @@ -68,7 +68,6 @@ def create_ticket(self, title=None, body=None, subject=None, priority=None): current_user = self.account.getCurrentUser() new_ticket = { 'subjectId': subject, - 'contents': body, 'assignedUserId': current_user['id'], 'title': title, } diff --git a/SoftLayer/managers/user.py b/SoftLayer/managers/user.py index 7031551c9..82cf62cd2 100644 --- a/SoftLayer/managers/user.py +++ b/SoftLayer/managers/user.py @@ -243,7 +243,15 @@ def create_user(self, user_object, password): :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) + + try: + return self.user_service.createObject(user_object, password, None) + except exceptions.SoftLayerAPIError as ex: + if ex.faultCode == "SoftLayer_Exception_User_Customer_DelegateIamIdInvitationToPaas": + raise exceptions.SoftLayerError("Your request for a new user was received, but it needs to be " + "processed by the Platform Services API first. Barring any errors on " + "the Platform Services side, your new user should be created shortly.") + raise def edit_user(self, user_id, user_object): """Blindly sends user_object to SoftLayer_User_Customer::editObject diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 059f06066..85bbdf9d9 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -18,8 +18,7 @@ LOGGER = logging.getLogger(__name__) - -# pylint: disable=no-self-use +# pylint: disable=no-self-use,too-many-lines class VSManager(utils.IdentifierMixin, object): @@ -51,6 +50,7 @@ def __init__(self, client, ordering_manager=None): 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) @@ -61,7 +61,7 @@ def __init__(self, client, ordering_manager=None): def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, memory=None, hostname=None, domain=None, local_disk=None, datacenter=None, nic_speed=None, - public_ip=None, private_ip=None, **kwargs): + public_ip=None, private_ip=None, transient=None, **kwargs): """Retrieve a list of all virtual servers on the account. Example:: @@ -88,6 +88,7 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, :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 boolean transient: filter on transient or non-transient instances :param dict \\*\\*kwargs: response-level options (mask, limit, etc.) :returns: Returns a list of dictionaries representing the matching virtual servers @@ -157,6 +158,11 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, _filter['virtualGuests']['primaryBackendIpAddress'] = ( utils.query_filter(private_ip)) + if transient is not None: + _filter['virtualGuests']['transientGuestFlag'] = ( + utils.query_filter(bool(transient)) + ) + kwargs['filter'] = _filter.to_dict() kwargs['iter'] = True return self.client.call('Account', call, **kwargs) @@ -194,6 +200,7 @@ def get_instance(self, instance_id, **kwargs): 'provisionDate,' 'notes,' 'dedicatedAccountHostOnlyFlag,' + 'transientGuestFlag,' 'privateNetworkOnlyFlag,' 'primaryBackendIpAddress,' 'primaryIpAddress,' @@ -209,6 +216,7 @@ def get_instance(self, instance_id, **kwargs): 'maxMemory,' 'datacenter,' 'activeTransaction[id, transactionStatus[friendlyName,name]],' + 'lastTransaction[transactionStatus],' 'lastOperatingSystemReload.id,' 'blockDevices,' 'blockDeviceTemplateGroup[id, name, globalIdentifier],' @@ -225,13 +233,15 @@ def get_instance(self, instance_id, **kwargs): 'hourlyBillingFlag,' 'userData,' '''billingItem[id,nextInvoiceTotalRecurringAmount, + package[id,keyName], children[categoryCode,nextInvoiceTotalRecurringAmount], orderItem[id, order.userRecord[username], preset.keyName]],''' 'tagReferences[id,tag[name,id]],' 'networkVlans[id,vlanNumber,networkSpace],' - 'dedicatedHost.id' + 'dedicatedHost.id,' + 'placementGroupId' ) return self.guest.getObject(id=instance_id, **kwargs) @@ -309,7 +319,7 @@ def _generate_create_dict( 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): + private_security_groups=None, boot_mode=None, transient=False, **kwargs): """Returns a dict appropriate to pass into Virtual_Guest::createObject See :func:`create_instance` for a list of available options. @@ -359,6 +369,9 @@ def _generate_create_dict( if private: data['privateNetworkOnlyFlag'] = private + if transient: + data['transientGuestFlag'] = transient + if image_id: data["blockDeviceTemplateGroup"] = {"globalIdentifier": image_id} elif os_code: @@ -425,14 +438,13 @@ def _create_network_components( 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)} + + 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)} + + parameters['primaryBackendNetworkComponent']['networkVlan']['primarySubnet'] = {'id': int(private_subnet)} return parameters @@ -500,15 +512,17 @@ def verify_create_instance(self, **kwargs): 'domain': u'test01.labs.sftlyr.ws', 'hostname': u'minion05', 'datacenter': u'hkg02', + 'flavor': 'BL1_1X2X100' 'dedicated': False, 'private': False, - 'cpus': 1, + 'transient': False, '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) @@ -533,15 +547,14 @@ def create_instance(self, **kwargs): '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] } @@ -604,17 +617,16 @@ def create_instances(self, config_list): # 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] } @@ -662,12 +674,10 @@ def change_port_speed(self, instance_id, public, speed): A port speed of 0 will disable the interface. """ if public: - return self.client.call('Virtual_Guest', - 'setPublicNetworkInterfaceSpeed', + return self.client.call('Virtual_Guest', 'setPublicNetworkInterfaceSpeed', speed, id=instance_id) else: - return self.client.call('Virtual_Guest', - 'setPrivateNetworkInterfaceSpeed', + return self.client.call('Virtual_Guest', 'setPrivateNetworkInterfaceSpeed', speed, id=instance_id) def _get_ids_from_hostname(self, hostname): @@ -782,10 +792,7 @@ def capture(self, instance_id, name, additional_disks=False, notes=None): continue # We never want swap devices - type_name = utils.lookup(block_device, - 'diskImage', - 'type', - 'keyName') + type_name = utils.lookup(block_device, 'diskImage', 'type', 'keyName') if type_name == 'SWAP': continue @@ -802,8 +809,7 @@ def capture(self, instance_id, name, additional_disks=False, notes=None): return self.guest.createArchiveTransaction( name, disks_to_capture, notes, id=instance_id) - def upgrade(self, instance_id, cpus=None, memory=None, - nic_speed=None, public=True): + def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=True, preset=None): """Upgrades a VS instance. Example:: @@ -817,6 +823,7 @@ def upgrade(self, instance_id, cpus=None, memory=None, :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. @@ -826,9 +833,27 @@ def upgrade(self, instance_id, cpus=None, memory=None, 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 ValueError("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 ValueError("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, @@ -841,23 +866,76 @@ def upgrade(self, instance_id, cpus=None, memory=None, "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 + def order_guest(self, guest_object, test=False): + """Uses Product_Order::placeOrder to create a virtual guest. + + Useful when creating a virtual guest with options not supported by Virtual_Guest::createObject + specifically ipv6 support. + + :param dictionary guest_object: See SoftLayer.CLI.virt.create._parse_create_args + + Example:: + + new_vsi = { + 'domain': u'test01.labs.sftlyr.ws', + 'hostname': u'minion05', + 'datacenter': u'hkg02', + 'flavor': 'BL1_1X2X100' + 'dedicated': False, + 'private': False, + 'transient': False, + 'os_code' : u'UBUNTU_LATEST', + 'hourly': True, + 'ssh_keys': [1234], + 'disks': ('100','25'), + 'local_disk': True, + 'tags': 'test, pleaseCancel', + 'public_security_groups': [12, 15], + 'ipv6': True + } + + vsi = mgr.order_guest(new_vsi) + # vsi will have the newly created vsi receipt. + # vsi['orderDetails']['virtualGuests'] will be an array of created Guests + print vsi + """ + tags = guest_object.pop('tags', None) + template = self.verify_create_instance(**guest_object) + + 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]}) + + # Notice this is `userdata` from the cli, but we send it in as `userData` + if guest_object.get('userdata'): + # SL_Virtual_Guest::generateOrderTemplate() doesn't respect userData, so we need to add it ourself + template['virtualGuests'][0]['userData'] = [{"value": guest_object.get('userdata')}] + if guest_object.get('host_id'): + template['hostId'] = guest_object.get('host_id') + if guest_object.get('placement_id'): + template['virtualGuests'][0]['placementGroupId'] = guest_object.get('placement_id') + + if test: + result = self.client.call('Product_Order', 'verifyOrder', template) + else: + result = self.client.call('Product_Order', 'placeOrder', template) + if tags is not None: + virtual_guests = utils.lookup(result, 'orderDetails', 'virtualGuests') + for guest in virtual_guests: + self.set_tags(tags, guest_id=guest['id']) + return result + def _get_package_items(self): """Following Method gets all the item ids related to VS. @@ -936,6 +1014,61 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public else: return price.get('id') + def get_summary_data_usage(self, instance_id, start_date=None, end_date=None, valid_type=None, summary_period=None): + """Retrieve the usage information of a virtual server. + + :param string instance_id: a string identifier used to resolve ids + :param string start_date: the start data to retrieve the vs usage information + :param string end_date: the start data to retrieve the vs usage information + :param string string valid_type: the Metric_Data_Type keyName. + :param int summary_period: summary period. + """ + valid_types = [ + { + "keyName": valid_type, + "summaryType": "max" + } + ] + + metric_tracking_id = self.get_tracking_id(instance_id) + + return self.client.call('Metric_Tracking_Object', 'getSummaryData', start_date, end_date, valid_types, + summary_period, id=metric_tracking_id, iter=True) + + def get_tracking_id(self, instance_id): + """Returns the Metric Tracking Object Id for a hardware server + + :param int instance_id: Id of the hardware server + """ + return self.guest.getMetricTrackingObjectId(id=instance_id) + + def get_bandwidth_data(self, instance_id, start_date=None, end_date=None, direction=None, rollup=3600): + """Gets bandwidth data for a server + + Will get averaged bandwidth data for a given time period. If you use a rollup over 3600 be aware + that the API will bump your start/end date to align with how data is stored. For example if you + have a rollup of 86400 your start_date will be bumped to 00:00. If you are not using a time in the + start/end date fields, this won't really matter. + + :param int instance_id: Hardware Id to get data for + :param date start_date: Date to start pulling data for. + :param date end_date: Date to finish pulling data for + :param string direction: Can be either 'public', 'private', or None for both. + :param int rollup: 300, 600, 1800, 3600, 43200 or 86400 seconds to average data over. + """ + tracking_id = self.get_tracking_id(instance_id) + data = self.client.call('Metric_Tracking_Object', 'getBandwidthData', start_date, end_date, direction, + rollup, id=tracking_id, iter=True) + return data + + def get_bandwidth_allocation(self, instance_id): + """Combines getBandwidthAllotmentDetail() and getBillingCycleBandwidthUsage() """ + a_mask = "mask[allocation[amount]]" + allotment = self.client.call('Virtual_Guest', 'getBandwidthAllotmentDetail', id=instance_id, mask=a_mask) + u_mask = "mask[amountIn,amountOut,type]" + useage = self.client.call('Virtual_Guest', 'getBillingCycleBandwidthUsage', id=instance_id, mask=u_mask) + return {'allotment': allotment['allocation'], 'useage': useage} + # pylint: disable=inconsistent-return-statements def _get_price_id_for_upgrade(self, package_items, option, value, public=True): """Find the price id for the option and value to upgrade. diff --git a/SoftLayer/managers/vs_capacity.py b/SoftLayer/managers/vs_capacity.py new file mode 100644 index 000000000..c2be6a615 --- /dev/null +++ b/SoftLayer/managers/vs_capacity.py @@ -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 --git a/SoftLayer/managers/vs_placement.py b/SoftLayer/managers/vs_placement.py new file mode 100644 index 000000000..d492b2a1e --- /dev/null +++ b/SoftLayer/managers/vs_placement.py @@ -0,0 +1,119 @@ +""" + SoftLayer.vs_placement + ~~~~~~~~~~~~~~~~~~~~~~~ + Placement Group Manager + + :license: MIT, see License for more details. +""" + +import logging + +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 PlacementManager(utils.IdentifierMixin, object): + """Manages SoftLayer Reserved Capacity Groups. + + Product Information + + - https://console.test.cloud.ibm.com/docs/vsi/vsi_placegroup.html#placement-groups + - https://softlayer.github.io/reference/services/SoftLayer_Account/getPlacementGroups/ + - https://softlayer.github.io/reference/services/SoftLayer_Virtual_PlacementGroup_Rule/ + + Existing instances cannot be added to a placement group. + You can only add a virtual server instance to a placement group at provisioning. + To remove an instance from a placement group, you must delete or reclaim the instance. + + :param SoftLayer.API.BaseClient client: the client instance + """ + + def __init__(self, client): + self.client = client + self.account = client['Account'] + self.resolvers = [self._get_id_from_name] + + def list(self, mask=None): + """List existing placement groups + + Calls SoftLayer_Account::getPlacementGroups + """ + if mask is None: + mask = "mask[id, name, createDate, rule, guestCount, backendRouter[id, hostname]]" + groups = self.client.call('Account', 'getPlacementGroups', mask=mask, iter=True) + return groups + + def create(self, placement_object): + """Creates a placement group + + A placement_object is defined as:: + + placement_object = { + 'backendRouterId': 12345, + 'name': 'Test Name', + 'ruleId': 12345 + } + + - https://softlayer.github.io/reference/datatypes/SoftLayer_Virtual_PlacementGroup/ + + :param dictionary placement_object: + + """ + return self.client.call('SoftLayer_Virtual_PlacementGroup', 'createObject', placement_object) + + def get_routers(self): + """Calls SoftLayer_Virtual_PlacementGroup::getAvailableRouters()""" + return self.client.call('SoftLayer_Virtual_PlacementGroup', 'getAvailableRouters') + + def get_object(self, group_id, mask=None): + """Returns a PlacementGroup Object + + https://softlayer.github.io/reference/services/SoftLayer_Virtual_PlacementGroup/getObject + """ + if mask is None: + mask = "mask[id, name, createDate, rule, backendRouter[id, hostname]," \ + "guests[activeTransaction[id,transactionStatus[name,friendlyName]]]]" + return self.client.call('SoftLayer_Virtual_PlacementGroup', 'getObject', id=group_id, mask=mask) + + def delete(self, group_id): + """Deletes a PlacementGroup + + Placement group must be empty to be deleted. + https://softlayer.github.io/reference/services/SoftLayer_Virtual_PlacementGroup/deleteObject + """ + return self.client.call('SoftLayer_Virtual_PlacementGroup', 'deleteObject', id=group_id) + + def get_all_rules(self): + """Returns all available rules for creating a placement group""" + return self.client.call('SoftLayer_Virtual_PlacementGroup_Rule', 'getAllObjects') + + def get_rule_id_from_name(self, name): + """Finds the rule that matches name. + + SoftLayer_Virtual_PlacementGroup_Rule.getAllObjects doesn't support objectFilters. + """ + results = self.client.call('SoftLayer_Virtual_PlacementGroup_Rule', 'getAllObjects') + return [result['id'] for result in results if result['keyName'] == name.upper()] + + def get_backend_router_id_from_hostname(self, hostname): + """Finds the backend router Id that matches the hostname given + + No way to use an objectFilter to find a backendRouter, so we have to search the hard way. + """ + results = self.client.call('SoftLayer_Network_Pod', 'getAllObjects') + return [result['backendRouterId'] for result in results if result['backendRouterName'] == hostname.lower()] + + def _get_id_from_name(self, name): + """List placement group ids which match the given name.""" + _filter = { + 'placementGroups': { + 'name': {'operation': name} + } + } + mask = "mask[id, name]" + results = self.client.call('Account', 'getPlacementGroups', filter=_filter, mask=mask) + return [result['id'] for result in results] diff --git a/SoftLayer/shell/cmd_help.py b/SoftLayer/shell/cmd_help.py index eeceef068..2f548d75d 100644 --- a/SoftLayer/shell/cmd_help.py +++ b/SoftLayer/shell/cmd_help.py @@ -22,6 +22,8 @@ def cli(ctx, env): shell_commands = [] for name in cli_core.cli.list_commands(ctx): command = cli_core.cli.get_command(ctx, name) + if command.short_help is None: + command.short_help = command.help details = (name, command.short_help) if name in dict(routes.ALL_ROUTES): shell_commands.append(details) diff --git a/SoftLayer/shell/completer.py b/SoftLayer/shell/completer.py index 1f59f3a53..fb94fd50e 100644 --- a/SoftLayer/shell/completer.py +++ b/SoftLayer/shell/completer.py @@ -24,18 +24,17 @@ def get_completions(self, document, complete_event): return _click_autocomplete(self.root, document.text_before_cursor) -# pylint: disable=stop-iteration-return def _click_autocomplete(root, text): """Completer generator for click applications.""" try: parts = shlex.split(text) except ValueError: - raise StopIteration + return location, incomplete = _click_resolve_command(root, parts) if not text.endswith(' ') and not incomplete and text: - raise StopIteration + return if incomplete and not incomplete[0:2].isalnum(): for param in location.params: diff --git a/SoftLayer/shell/core.py b/SoftLayer/shell/core.py index ed90f9c95..55a56e888 100644 --- a/SoftLayer/shell/core.py +++ b/SoftLayer/shell/core.py @@ -13,8 +13,8 @@ import traceback import click -from prompt_toolkit import auto_suggest as p_auto_suggest -from prompt_toolkit import shortcuts as p_shortcuts +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit import PromptSession from SoftLayer.CLI import core from SoftLayer.CLI import environment @@ -26,7 +26,6 @@ class ShellExit(Exception): """Exception raised to quit the shell.""" - pass @click.command() @@ -49,12 +48,14 @@ def cli(ctx, env): os.makedirs(app_path) complete = completer.ShellCompleter(core.cli) + session = PromptSession() + while True: try: - line = p_shortcuts.prompt( + line = session.prompt( completer=complete, complete_while_typing=True, - auto_suggest=p_auto_suggest.AutoSuggestFromHistory(), + auto_suggest=AutoSuggestFromHistory(), ) # Parse arguments diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py index 477815725..87d7f5e41 100644 --- a/SoftLayer/testing/__init__.py +++ b/SoftLayer/testing/__init__.py @@ -96,11 +96,9 @@ def tearDownClass(cls): def set_up(self): """Aliased from setUp.""" - pass def tear_down(self): """Aliased from tearDown.""" - pass def setUp(self): # NOQA testtools.TestCase.setUp(self) @@ -159,7 +157,7 @@ def set_mock(self, service, method): """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', stdin=None): """A helper that runs a SoftLayer CLI command. This returns a click.testing.Result object. @@ -171,7 +169,7 @@ def run_command(self, args=None, env=None, fixtures=True, fmt='json'): args.insert(0, '--format=%s' % fmt) runner = testing.CliRunner() - return runner.invoke(core.cli, args=args, obj=env or self.env) + return runner.invoke(core.cli, args=args, input=stdin, obj=env or self.env) def call_has_props(call, props): diff --git a/SoftLayer/testing/xmlrpc.py b/SoftLayer/testing/xmlrpc.py index 257a6be75..bd74afe93 100644 --- a/SoftLayer/testing/xmlrpc.py +++ b/SoftLayer/testing/xmlrpc.py @@ -80,7 +80,6 @@ def do_POST(self): def log_message(self, fmt, *args): """Override log_message.""" - pass def _item_by_key_postfix(dictionary, key_prefix): diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 3aa896f11..b4790c60e 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -34,7 +34,7 @@ ] REST_SPECIAL_METHODS = { - 'deleteObject': 'DELETE', + # 'deleteObject': 'DELETE', 'createObject': 'POST', 'createObjects': 'POST', 'editObject': 'PUT', @@ -170,6 +170,10 @@ def __call__(self, request): largs = list(request.args) headers = request.headers + auth = None + if request.transport_user: + auth = requests.auth.HTTPBasicAuth(request.transport_user, request.transport_password) + if request.identifier is not None: header_name = request.service + 'InitParameters' headers[header_name] = {'id': request.identifier} @@ -208,6 +212,7 @@ def __call__(self, request): try: resp = self.client.request('POST', request.url, data=request.payload, + auth=auth, headers=request.transport_headers, timeout=self.timeout, verify=request.verify, @@ -253,6 +258,7 @@ def print_reproduceable(self, request): from string import Template output = Template('''============= testing.py ============= import requests +from requests.auth import HTTPBasicAuth from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from xml.etree import ElementTree @@ -261,6 +267,9 @@ def print_reproduceable(self, request): retry = Retry(connect=3, backoff_factor=3) adapter = HTTPAdapter(max_retries=retry) client.mount('https://', adapter) +# This is only needed if you are using an cloud.ibm.com api key +#auth=HTTPBasicAuth('apikey', YOUR_CLOUD_API_KEY) +auth=None url = '$url' payload = """$payload""" transport_headers = $transport_headers @@ -269,7 +278,7 @@ def print_reproduceable(self, request): cert = $cert proxy = $proxy response = client.request('POST', url, data=payload, headers=transport_headers, timeout=timeout, - verify=verify, cert=cert, proxies=proxy) + verify=verify, cert=cert, proxies=proxy, auth=auth) xml = ElementTree.fromstring(response.content) ElementTree.dump(xml) ==========================''') @@ -379,7 +388,15 @@ def __call__(self, request): request.url = resp.url resp.raise_for_status() - result = json.loads(resp.text) + + if resp.text != "": + try: + result = json.loads(resp.text) + except ValueError as json_ex: + raise exceptions.SoftLayerAPIError(resp.status_code, str(json_ex)) + else: + raise exceptions.SoftLayerAPIError(resp.status_code, "Empty response.") + request.result = result if isinstance(result, list): @@ -388,8 +405,15 @@ def __call__(self, request): else: return result except requests.HTTPError as ex: - message = json.loads(ex.response.text)['error'] - request.url = ex.response.url + try: + message = json.loads(ex.response.text)['error'] + request.url = ex.response.url + except ValueError as json_ex: + if ex.response.text == "": + raise exceptions.SoftLayerAPIError(resp.status_code, "Empty response.") + + raise exceptions.SoftLayerAPIError(resp.status_code, str(json_ex)) + raise exceptions.SoftLayerAPIError(ex.response.status_code, message) except requests.RequestException as ex: raise exceptions.TransportError(0, str(ex)) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 131c681f1..b70997842 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -7,6 +7,7 @@ """ import datetime import re +import time import six @@ -121,8 +122,72 @@ def query_filter_date(start, end): return { 'operation': 'betweenDate', 'options': [ - {'name': 'startDate', 'value': [startdate+' 0:0:0']}, - {'name': 'endDate', 'value': [enddate+' 0:0:0']} + {'name': 'startDate', 'value': [startdate + ' 0:0:0']}, + {'name': 'endDate', 'value': [enddate + ' 0:0:0']} + ] + } + + +def format_event_log_date(date_string, utc): + """Gets a date in the format that the SoftLayer_EventLog object likes. + + :param string date_string: date in mm/dd/yyyy format + :param string utc: utc offset. Defaults to '+0000' + """ + user_date_format = "%m/%d/%Y" + + user_date = datetime.datetime.strptime(date_string, user_date_format) + dirty_time = user_date.isoformat() + + if utc is None: + utc = "+0000" + + iso_time_zone = utc[:3] + ':' + utc[3:] + cleaned_time = "{}.000000{}".format(dirty_time, iso_time_zone) + + return cleaned_time + + +def event_log_filter_between_date(start, end, utc): + """betweenDate Query filter that SoftLayer_EventLog likes + + :param string start: lower bound date in mm/dd/yyyy format + :param string end: upper bound date in mm/dd/yyyy format + :param string utc: utc offset. Defaults to '+0000' + """ + return { + 'operation': 'betweenDate', + 'options': [ + {'name': 'startDate', 'value': [format_event_log_date(start, utc)]}, + {'name': 'endDate', 'value': [format_event_log_date(end, utc)]} + ] + } + + +def event_log_filter_greater_than_date(date, utc): + """greaterThanDate Query filter that SoftLayer_EventLog likes + + :param string date: lower bound date in mm/dd/yyyy format + :param string utc: utc offset. Defaults to '+0000' + """ + return { + 'operation': 'greaterThanDate', + 'options': [ + {'name': 'date', 'value': [format_event_log_date(date, utc)]} + ] + } + + +def event_log_filter_less_than_date(date, utc): + """lessThanDate Query filter that SoftLayer_EventLog likes + + :param string date: upper bound date in mm/dd/yyyy format + :param string utc: utc offset. Defaults to '+0000' + """ + return { + 'operation': 'lessThanDate', + 'options': [ + {'name': 'date', 'value': [format_event_log_date(date, utc)]} ] } @@ -224,3 +289,53 @@ def clean_string(string): return '' else: return " ".join(string.split()) + + +def clean_splitlines(string): + """Returns a string where \r\n is replaced with \n""" + if string is None: + return '' + else: + return "\n".join(string.splitlines()) + + +def clean_time(sltime, in_format='%Y-%m-%dT%H:%M:%S%z', out_format='%Y-%m-%d %H:%M'): + """Easy way to format time strings + + :param string sltime: A softlayer formatted time string + :param string in_format: Datetime format for strptime + :param string out_format: Datetime format for strftime + """ + try: + clean = datetime.datetime.strptime(sltime, in_format) + return clean.strftime(out_format) + # The %z option only exists with py3.6+ + except ValueError: + return sltime + + +def timestamp(date): + """Converts a datetime to timestamp + + :param datetime date: + :returns int: The timestamp of date. + """ + + _timestamp = time.mktime(date.timetuple()) + + return int(_timestamp) + + +def days_to_datetime(days): + """Returns the datetime value of last N days. + + :param int days: From 0 to N days + :returns int: The datetime of last N days or datetime.now() if days <= 0. + """ + + date = datetime.datetime.now() + + if days > 0: + date -= datetime.timedelta(days=days) + + return date diff --git a/docs/api/client.rst b/docs/api/client.rst index a29974be2..c798ac71d 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -8,7 +8,7 @@ and executing XML-RPC calls against the SoftLayer API. Below are some links that will help to use the SoftLayer API. -* `SoftLayer API Documentation `_ +* `SoftLayer API Documentation `_ * `Source on GitHub `_ :: @@ -86,9 +86,9 @@ offsets, and retrieving objects by id. The following section assumes you have an initialized client named 'client'. The best way to test our setup is to call the -`getObject `_ +`getObject `_ method on the -`SoftLayer_Account `_ +`SoftLayer_Account `_ service. :: @@ -97,7 +97,7 @@ service. For a more complex example we'll retrieve a support ticket with id 123456 along with the ticket's updates, the user it's assigned to, the servers attached to it, and the datacenter those servers are in. To retrieve our extra information -using an `object mask `_. +using an `object mask `_. Retrieve a ticket using object masks. :: @@ -106,22 +106,28 @@ Retrieve a ticket using object masks. id=123456, mask="updates, assignedUser, attachedHardware.datacenter") -Now add an update to the ticket with -`Ticket.addUpdate `_. +Now add an update to the ticket with `Ticket.addUpdate `_. This uses a parameter, which translate to positional arguments in the order that they appear in the API docs. + + :: update = client.call('Ticket', 'addUpdate', {'entry' : 'Hello!'}, id=123456) Let's get a listing of virtual guests using the domain example.com + + :: client.call('Account', 'getVirtualGuests', filter={'virtualGuests': {'domain': {'operation': 'example.com'}}}) -This call gets tickets created between the beginning of March 1, 2013 and -March 15, 2013. +This call gets tickets created between the beginning of March 1, 2013 and March 15, 2013. +More information on `Object Filters `_. + +:NOTE: The `value` field for startDate and endDate is in `[]`, if you do not put the date in brackets the filter will not work. + :: client.call('Account', 'getTickets', @@ -141,11 +147,24 @@ March 15, 2013. SoftLayer's XML-RPC API also allows for pagination. :: - client.call('Account', 'getVirtualGuests', limit=10, offset=0) # Page 1 - client.call('Account', 'getVirtualGuests', limit=10, offset=10) # Page 2 + from pprint import pprint + + page1 = client.call('Account', 'getVirtualGuests', limit=10, offset=0) # Page 1 + page2 = client.call('Account', 'getVirtualGuests', limit=10, offset=10) # Page 2 + + #Automatic Pagination (v5.5.3+), default limit is 100 + result = client.call('Account', 'getVirtualGuests', iter=True, limit=10) + pprint(result) + + # Using a python generator, default limit is 100 + results = client.iter_call('Account', 'getVirtualGuests', limit=10) + for result in results: + pprint(result) + +:NOTE: `client.call(iter=True)` will pull all results, then return. `client.iter_call()` will return a generator, and only make API calls as you iterate over the results. Here's how to create a new Cloud Compute Instance using -`SoftLayer_Virtual_Guest.createObject `_. +`SoftLayer_Virtual_Guest.createObject `_. Be warned, this call actually creates an hourly virtual server so this will have billing implications. :: @@ -161,6 +180,28 @@ have billing implications. }) +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 --git a/docs/api/managers/account.rst b/docs/api/managers/account.rst new file mode 100644 index 000000000..25d76ed6a --- /dev/null +++ b/docs/api/managers/account.rst @@ -0,0 +1,5 @@ +.. _account: + +.. automodule:: SoftLayer.managers.account + :members: + :inherited-members: \ No newline at end of file diff --git a/docs/api/managers/event_log.rst b/docs/api/managers/event_log.rst new file mode 100644 index 000000000..41adfeaa4 --- /dev/null +++ b/docs/api/managers/event_log.rst @@ -0,0 +1,5 @@ +.. _event_log: + +.. automodule:: SoftLayer.managers.event_log + :members: + :inherited-members: diff --git a/docs/api/managers/messaging.rst b/docs/api/managers/messaging.rst deleted file mode 100644 index f86bbc980..000000000 --- a/docs/api/managers/messaging.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. _messaging: - -.. automodule:: SoftLayer.managers.messaging - :members: - :inherited-members: diff --git a/docs/api/managers/vs_capacity.rst b/docs/api/managers/vs_capacity.rst new file mode 100644 index 000000000..3255a40b1 --- /dev/null +++ b/docs/api/managers/vs_capacity.rst @@ -0,0 +1,5 @@ +.. _vs_capacity: + +.. automodule:: SoftLayer.managers.vs_capacity + :members: + :inherited-members: diff --git a/docs/api/managers/vs_placement.rst b/docs/api/managers/vs_placement.rst new file mode 100644 index 000000000..d5898f1f0 --- /dev/null +++ b/docs/api/managers/vs_placement.rst @@ -0,0 +1,5 @@ +.. _vs_placement: + +.. automodule:: SoftLayer.managers.vs_placement + :members: + :inherited-members: diff --git a/docs/cli.rst b/docs/cli.rst index 7701dc625..7b09fdc17 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -11,12 +11,9 @@ functionality not fully documented here. .. toctree:: :maxdepth: 2 + :glob: - cli/ipsec - cli/vs - cli/ordering - cli/users - + cli/* .. _config_setup: @@ -34,7 +31,7 @@ To update the configuration, you can use `slcli setup`. :..............:..................................................................: : Username : username : : API Key : oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha : - : Endpoint URL : https://api.softlayer.com/xmlrpc/v3/ : + : Endpoint URL : https://api.softlayer.com/xmlrpc/v3.1/ : :..............:..................................................................: Are you sure you want to write settings to "/home/me/.softlayer"? [y/N]: y @@ -47,10 +44,12 @@ To check the configuration, you can use `slcli config show`. :..............:..................................................................: : Username : username : : API Key : oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha : - : Endpoint URL : https://api.softlayer.com/xmlrpc/v3/ : + : Endpoint URL : https://api.softlayer.com/xmlrpc/v3.1/ : :..............:..................................................................: +If you are using an account created from the https://cloud.ibm.com portal, your username will be literally `apikey`, and use the key provided. `How to create an IBM apikey `_ + To see more about the config file format, see :ref:`config_file`. .. _usage-examples: @@ -60,43 +59,43 @@ Usage Examples To discover the available commands, simply type `slcli`. :: - $ slcli + $ slcli Usage: slcli [OPTIONS] COMMAND [ARGS]... - + SoftLayer Command-line Client - + Options: - --format [table|raw|json|jsonraw] - Output format [default: table] - -C, --config PATH Config file location [default: - ~/.softlayer] - -v, --verbose Sets the debug noise level, specify multiple - times for more verbosity. - --proxy TEXT HTTP[S] proxy to be use to make API calls - -y, --really / --not-really Confirm all prompt actions - --demo / --no-demo Use demo data instead of actually making API - calls - --version Show the version and exit. - -h, --help Show this message and exit. - + --format [table|raw|json|jsonraw] Output format [default: raw] + -C, --config PATH Config file location [default: ~\.softlayer] + -v, --verbose Sets the debug noise level, specify multiple times for more verbosity. + --proxy TEXT HTTP[S] proxy to be use to make API calls + -y, --really / --not-really Confirm all prompt actions + --demo / --no-demo Use demo data instead of actually making API calls + --version Show the version and exit. + -h, --help Show this message and exit. + Commands: block Block Storage. call-api Call arbitrary API endpoints. cdn Content Delivery Network. config CLI configuration. + dedicatedhost Dedicated Host. dns Domain Name System. + event-log Event Logs. file File Storage. firewall Firewalls. globalip Global IP addresses. hardware Hardware servers. image Compute images. + ipsec IPSEC VPN loadbal Load balancers. - messaging Message queue service. metadata Find details about this machine. nas Network Attached Storage. object-storage Object Storage. + order View and order from the catalog. report Reports. rwhois Referral Whois. + securitygroup Network security groups. setup Edit configuration. shell Enters a shell for slcli. sshkey SSH Keys. @@ -104,9 +103,10 @@ To discover the available commands, simply type `slcli`. subnet Network subnets. summary Account summary. ticket Support tickets. + user Manage Users. virtual Virtual Servers. vlan Network VLANs. - + To use most commands your SoftLayer username and api_key need to be configured. The easiest way to do that is to use: 'slcli setup' @@ -177,3 +177,75 @@ Most commands will take in additional options/arguments. To see all available ac --tags TEXT Show instances that have one of these comma- separated tags --help Show this message and exit. + + + +Debugging +========= +To see exactly what API call is being made by the SLCLI, you can use the verbose option. + +A single `-v` will show a simple version of the API call, along with some statistics + +:: + + slcli -v vs detail 74397127 + Calling: SoftLayer_Virtual_Guest::getObject(id=74397127, mask='id,globalIdentifier,fullyQualifiedDomainName,hostname,domain', filter='None', args=(), limit=None, offset=None)) + Calling: SoftLayer_Virtual_Guest::getReverseDomainRecords(id=77460683, mask='', filter='None', args=(), limit=None, offset=None)) + :..................:..............................................................: + : name : value : + :..................:..............................................................: + : execution_time : 2.020334s : + : api_calls : SoftLayer_Virtual_Guest::getObject (1.515583s) : + : : SoftLayer_Virtual_Guest::getReverseDomainRecords (0.494480s) : + : version : softlayer-python/v5.7.2 : + : python_version : 3.7.3 (default, Mar 27 2019, 09:23:15) : + : : [Clang 10.0.1 (clang-1001.0.46.3)] : + : library_location : /Users/chris/Code/py3/lib/python3.7/site-packages/SoftLayer : + :..................:..............................................................: + + +Using `-vv` will print out some API call details in the summary as well. + +:: + + slcli -vv account summary + Calling: SoftLayer_Account::getObject(id=None, mask='mask[ nextInvoiceTotalAmount, pendingInvoice[invoiceTotalAmount], blockDeviceTemplateGroupCount, dedicatedHostCount, domainCount, hardwareCount, networkStorageCount, openTicketCount, networkVlanCount, subnetCount, userCount, virtualGuestCount ]', filter='None', args=(), limit=None, offset=None)) + :..................:.............................................................: + : name : value : + :..................:.............................................................: + : execution_time : 0.921271s : + : api_calls : SoftLayer_Account::getObject (0.911208s) : + : version : softlayer-python/v5.7.2 : + : python_version : 3.7.3 (default, Mar 27 2019, 09:23:15) : + : : [Clang 10.0.1 (clang-1001.0.46.3)] : + : library_location : /Users/chris/Code/py3/lib/python3.7/site-packages/SoftLayer : + :..................:.............................................................: + :........:.................................................: + : : SoftLayer_Account::getObject : + :........:.................................................: + : id : None : + : mask : mask[ : + : : nextInvoiceTotalAmount, : + : : pendingInvoice[invoiceTotalAmount], : + : : blockDeviceTemplateGroupCount, : + : : dedicatedHostCount, : + : : domainCount, : + : : hardwareCount, : + : : networkStorageCount, : + : : openTicketCount, : + : : networkVlanCount, : + : : subnetCount, : + : : userCount, : + : : virtualGuestCount : + : : ] : + : filter : None : + : limit : None : + : offset : None : + :........:.................................................: + +Using `-vvv` will print out the exact API that can be used without the softlayer-python framework, A simple python code snippet for XML-RPC, a curl call for REST API calls. This is dependant on the endpoint you are using in the config file. + +:: + + slcli -vvv account summary + curl -u $SL_USER:$SL_APIKEY -X GET -H "Accept: */*" -H "Accept-Encoding: gzip, deflate, compress" 'https://api.softlayer.com/rest/v3.1/SoftLayer_Account/getObject.json?objectMask=mask%5B%0A++++++++++++nextInvoiceTotalAmount%2C%0A++++++++++++pendingInvoice%5BinvoiceTotalAmount%5D%2C%0A++++++++++++blockDeviceTemplateGroupCount%2C%0A++++++++++++dedicatedHostCount%2C%0A++++++++++++domainCount%2C%0A++++++++++++hardwareCount%2C%0A++++++++++++networkStorageCount%2C%0A++++++++++++openTicketCount%2C%0A++++++++++++networkVlanCount%2C%0A++++++++++++subnetCount%2C%0A++++++++++++userCount%2C%0A++++++++++++virtualGuestCount%0A++++++++++++%5D' diff --git a/docs/cli/account.rst b/docs/cli/account.rst new file mode 100644 index 000000000..9b3ad6954 --- /dev/null +++ b/docs/cli/account.rst @@ -0,0 +1,25 @@ +.. _cli_account: + +Account Commands +================= + + +.. click:: SoftLayer.CLI.account.summary:cli + :prog: account summary + :show-nested: + +.. click:: SoftLayer.CLI.account.events:cli + :prog: account events + :show-nested: + +.. click:: SoftLayer.CLI.account.event_detail:cli + :prog: account event-detail + :show-nested: + +.. click:: SoftLayer.CLI.account.invoices:cli + :prog: account invoices + :show-nested: + +.. click:: SoftLayer.CLI.account.invoice_detail:cli + :prog: account invoice-detail + :show-nested: \ No newline at end of file diff --git a/docs/cli/call_api.rst b/docs/cli/call_api.rst new file mode 100644 index 000000000..e309f16eb --- /dev/null +++ b/docs/cli/call_api.rst @@ -0,0 +1,9 @@ +.. _cli_call_api: + +Call API +======== + + +.. click:: SoftLayer.CLI.call_api:cli + :prog: call-api + :show-nested: diff --git a/docs/cli/cdn.rst b/docs/cli/cdn.rst new file mode 100644 index 000000000..fce54f731 --- /dev/null +++ b/docs/cli/cdn.rst @@ -0,0 +1,29 @@ +.. _cli_cdn: + +Interacting with CDN +============================== + + +.. click:: SoftLayer.CLI.cdn.detail:cli + :prog: cdn detail + :show-nested: + +.. click:: SoftLayer.CLI.cdn.list:cli + :prog: cdn list + :show-nested: + +.. click:: SoftLayer.CLI.cdn.origin_add:cli + :prog: cdn origin-add + :show-nested: + +.. click:: SoftLayer.CLI.cdn.origin_list:cli + :prog: cdn origin-list + :show-nested: + +.. click:: SoftLayer.CLI.cdn.origin_remove:cli + :prog: cdn origin-remove + :show-nested: + +.. click:: SoftLayer.CLI.cdn.purge:cli + :prog: cdn purge + :show-nested: diff --git a/docs/cli/config.rst b/docs/cli/config.rst new file mode 100644 index 000000000..b49e5d5ad --- /dev/null +++ b/docs/cli/config.rst @@ -0,0 +1,18 @@ +.. _cli_config: + +Config +======== + +`Creating an IBMID apikey `_ +`IBMid for services `_ + +`Creating a SoftLayer apikey `_ + +.. click:: SoftLayer.CLI.config.setup:cli + :prog: config setup + :show-nested: + + +.. click:: SoftLayer.CLI.config.show:cli + :prog: config show + :show-nested: diff --git a/docs/cli/event_log.rst b/docs/cli/event_log.rst new file mode 100644 index 000000000..47c7639e5 --- /dev/null +++ b/docs/cli/event_log.rst @@ -0,0 +1,36 @@ +.. _cli_event_log: + +Event-Log Commands +==================== + + +.. click:: SoftLayer.CLI.event_log.get:cli + :prog: event-log get + :show-nested: + +There are usually quite a few events on an account, so be careful when using the `--limit -1` option. The command will automatically break requests out into smaller sub-requests, but this command may take a very long time to complete. It will however print out data as it comes in. + +.. click:: SoftLayer.CLI.event_log.types:cli + :prog: event-log types + :show-nested: + + +Currently the types are as follows, more may be added in the future. +:: + + :......................: + : types : + :......................: + : Account : + : CDN : + : User : + : Bare Metal Instance : + : API Authentication : + : Server : + : CCI : + : Image : + : Bluemix LB : + : Facility : + : Cloud Object Storage : + : Security Group : + :......................: \ No newline at end of file diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst new file mode 100644 index 000000000..3e7eeaf4c --- /dev/null +++ b/docs/cli/hardware.rst @@ -0,0 +1,96 @@ +.. _cli_hardware: + +Interacting with Hardware +============================== + + +.. click:: SoftLayer.CLI.hardware.bandwidth:cli + :prog: hw bandwidth + :show-nested: + +.. click:: SoftLayer.CLI.hardware.cancel_reasons:cli + :prog: hw cancel-reasons + :show-nested: + +.. click:: SoftLayer.CLI.hardware.cancel:cli + :prog: hw cancel + :show-nested: + +.. click:: SoftLayer.CLI.hardware.create_options:cli + :prog: hw create-options + :show-nested: + +.. click:: SoftLayer.CLI.hardware.create:cli + :prog: hw create + :show-nested: + + +Provides some basic functionality to order a server. `slcli order` has a more full featured method of ordering servers. This command only supports the FAST_PROVISION type. + +.. click:: SoftLayer.CLI.hardware.credentials:cli + :prog: hw credentials + :show-nested: + + +.. click:: SoftLayer.CLI.hardware.detail:cli + :prog: hw detail + :show-nested: + + +.. click:: SoftLayer.CLI.hardware.edit:cli + :prog: hw edit + :show-nested: + +When setting port speed, use "-1" to indicate best possible configuration. Using 10/100/1000/10000 on a server with a redundant interface may result the interface entering a degraded state. See `setPublicNetworkInterfaceSpeed `_ for more information. + + +.. click:: SoftLayer.CLI.hardware.list:cli + :prog: hw list + :show-nested: + +.. click:: SoftLayer.CLI.hardware.power:power_cycle + :prog: hw power-cycle + :show-nested: + +.. click:: SoftLayer.CLI.hardware.power:power_off + :prog: hw power-off + :show-nested: + +.. click:: SoftLayer.CLI.hardware.power:power_on + :prog: hw power-on + :show-nested: + +.. click:: SoftLayer.CLI.hardware.power:reboot + :prog: hw reboot + :show-nested: + +.. click:: SoftLayer.CLI.hardware.reload:cli + :prog: hw reload + :show-nested: + +.. click:: SoftLayer.CLI.hardware.power:rescue + :prog: hw rescue + +.. click:: SoftLayer.CLI.hardware.reflash_firmware:cli + :prog: hw reflash-firmware + :show-nested: + + +Reflash here means the current version of the firmware running on your server will be re-flashed onto the selected hardware. This does require a reboot. See `slcli hw update-firmware` if you want the newest version. + +.. click:: SoftLayer.CLI.hardware.update_firmware:cli + :prog: hw update-firmware + :show-nested: + + +This function updates the firmware of a server. If already at the latest version, no software is installed. + +.. click:: SoftLayer.CLI.hardware.toggle_ipmi:cli + :prog: hw toggle-ipmi + :show-nested: + + +.. click:: SoftLayer.CLI.hardware.ready:cli + :prog: hw ready + :show-nested: + diff --git a/docs/cli/ordering.rst b/docs/cli/ordering.rst index 0724cb60f..acaf3a07e 100644 --- a/docs/cli/ordering.rst +++ b/docs/cli/ordering.rst @@ -1,7 +1,7 @@ .. _cli_order: Ordering -========== +======== The Order :ref:`cli` commands can be used to build an order for any product in the SoftLayer catalog. The basic flow for ordering goes something like this... @@ -11,11 +11,14 @@ The basic flow for ordering goes something like this... #. item-list #. place -.. _cli_ordering_package_list: -order package-list ------------------- -This command will list all of the packages that are available to be ordered. This is the starting point for placing any order. Find the package keyName you want to order, and use it for the next steps. + + + +.. click:: SoftLayer.CLI.order.package_list:cli + :prog: order package-list + :show-nested: + .. note:: * CLOUD_SERVER: These are Virtual Servers @@ -27,10 +30,15 @@ This command will list all of the packages that are available to be ordered. Thi Bluemix services listed here may still need to be ordered through the Bluemix CLI/Portal -.. _cli_ordering_category_list: -order category-list -------------------- +.. click:: SoftLayer.CLI.order.package_locations:cli + :prog: order package-locations + :show-nested: + +.. click:: SoftLayer.CLI.order.category_list:cli + :prog: order category-list + :show-nested: + Shows all the available categories for a certain package, useful in finding the required categories. Categories that are required will need to have a corresponding item included with any orders These are all the required categories for ``BARE_METAL_SERVER`` @@ -52,23 +60,22 @@ These are all the required categories for ``BARE_METAL_SERVER`` : VPN Management - Private Network : vpn_management : Y : :........................................:.......................:............: -.. _cli_ordering_item_list: +.. click:: SoftLayer.CLI.order.item_list:cli + :prog: order item-list + :show-nested: -order item-list ---------------- Shows all the prices for a given package. Collect all the items you want included on your server. Don't forget to include the required category items. If forgotten, ``order place`` will tell you about it. -.. _cli_ordering_preset_list: +.. click:: SoftLayer.CLI.order.preset_list:cli + :prog: order preset-list + :show-nested: -order preset-list ------------------ -Some packages have presets which makes ordering significantly simpler. These will have set CPU / RAM / Disk allotments. You still need to specify required items -.. _cli_ordering_place: +.. click:: SoftLayer.CLI.order.place:cli + :prog: order place + :show-nested: -order place ------------ Now that you have the package you want, the prices needed, and found a location, it is time to place an order. order place @@ -85,6 +92,7 @@ order place --extras '{"hardware": [{"hostname" : "testOrder", "domain": "cgallo.com"}]}' \ --complex-type SoftLayer_Container_Product_Order_Hardware_Server + order place ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -105,4 +113,25 @@ order place 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 \ No newline at end of file + --complex-type SoftLayer_Container_Product_Order_Virtual_Guest + + + +Quotes +====== +.. click:: SoftLayer.CLI.order.quote:cli + :prog: order quote + :show-nested: + + +.. click:: SoftLayer.CLI.order.quote_list:cli + :prog: order quote-list + :show-nested: + +.. click:: SoftLayer.CLI.order.quote_detail:cli + :prog: order quote-detail + :show-nested: + +.. click:: SoftLayer.CLI.order.place_quote:cli + :prog: order place-quote + :show-nested: diff --git a/docs/cli/reports.rst b/docs/cli/reports.rst new file mode 100644 index 000000000..f62de5882 --- /dev/null +++ b/docs/cli/reports.rst @@ -0,0 +1,17 @@ +.. _cli_reports: + +Reports +======= + +There are a few report type commands in the SLCLI. + +.. click:: SoftLayer.CLI.summary:cli + :prog: summary + :show-nested: + +A list of datacenters, and how many servers, VSI, vlans, subnets and public_ips are in each. + + +.. click:: SoftLayer.CLI.report.bandwidth:cli + :prog: report bandwidth + :show-nested: \ No newline at end of file diff --git a/docs/cli/users.rst b/docs/cli/users.rst index 44cd71551..3c98199a7 100644 --- a/docs/cli/users.rst +++ b/docs/cli/users.rst @@ -5,6 +5,7 @@ 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. @@ -19,6 +20,7 @@ Options -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: @@ -83,11 +85,12 @@ 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] + +-t, --template TEXT A json string describing `SoftLayer_User_Customer `_ . [required] -h, --help Show this message and exit. diff --git a/docs/cli/vs.rst b/docs/cli/vs.rst index f61b9fd92..f855238d5 100644 --- a/docs/cli/vs.rst +++ b/docs/cli/vs.rst @@ -28,6 +28,8 @@ virtual server (VS), we need to know what options are available to us: RAM, 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 @@ Luckily, there's a simple command to show all options: `slcli vs create-options` :................................:.................................................................................: : 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 @@ Luckily, there's a simple command to show all options: `slcli vs create-options` : : 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 @@ Luckily, there's a simple command to show all options: `slcli vs create-options` : : 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,15 +81,33 @@ datacenter using the command `slcli vs create`. :: - $ 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 -f B1_1X2X25 -o DEBIAN_LATEST_64 --datacenter=ams01 --billing=hourly This action will incur charges on your account. Continue? [y/N]: y - :.........:......................................: - : name : value : - :.........:......................................: - : id : 1234567 : - : created : 2013-06-13T08:29:44-06:00 : - : guid : 6e013cde-a863-46ee-8s9a-f806dba97c89 : - :.........:......................................: + :..........:.................................:......................................:...........................: + : ID : FQDN : guid : Order Date : + :..........:.................................:......................................:...........................: + : 70112999 : testtesttest.test.com : 1abc7afb-9618-4835-89c9-586f3711d8ea : 2019-01-30T17:16:58-06:00 : + :..........:.................................:......................................:...........................: + :.........................................................................: + : OrderId: 12345678 : + :.......:.................................................................: + : Cost : Description : + :.......:.................................................................: + : 0.0 : Debian GNU/Linux 9.x Stretch/Stable - Minimal Install (64 bit) : + : 0.0 : 25 GB (SAN) : + : 0.0 : Reboot / Remote Console : + : 0.0 : 100 Mbps Public & Private Network Uplinks : + : 0.0 : 0 GB Bandwidth Allotment : + : 0.0 : 1 IP Address : + : 0.0 : Host Ping and TCP Service Monitoring : + : 0.0 : Email and Ticket : + : 0.0 : Automated Reboot from Monitoring : + : 0.0 : Unlimited SSL VPN Users & 1 PPTP VPN User per account : + : 0.0 : Nessus Vulnerability Assessment & Reporting : + : 0.0 : 2 GB : + : 0.0 : 1 x 2.0 GHz or higher Core : + : 0.000 : Total hourly cost : + :.......:.................................................................: After the last command, the virtual server is now being built. It should @@ -301,7 +119,7 @@ instantly appear in your virtual server list now. :.........:............:.......................:.......:........:................:..............:....................: : 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 +156,12 @@ username is 'root' and password is 'ABCDEFGH'. : 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 : @@ -352,36 +170,94 @@ username is 'root' and password is 'ABCDEFGH'. :..............:...........................: -There are many other commands to help manage virtual servers. To see them all, -use `slcli help vs`. -:: +.. click:: SoftLayer.CLI.virt.bandwidth:cli + :prog: vs bandwidth + :show-nested: + +If no timezone is specified, IMS local time (CST) will be assumed, which might not match your user's selected timezone. + + +.. click:: SoftLayer.CLI.virt.cancel:cli + :prog: vs cancel + :show-nested: + +.. click:: SoftLayer.CLI.virt.capture:cli + :prog: vs capture + :show-nested: + +.. click:: SoftLayer.CLI.virt.create:cli + :prog: vs create + :show-nested: + +.. click:: SoftLayer.CLI.virt.create_options:cli + :prog: vs create-options + :show-nested: + +.. click:: SoftLayer.CLI.virt.dns:cli + :prog: vs dns-sync + :show-nested: + +.. click:: SoftLayer.CLI.virt.edit:cli + :prog: vs edit + :show-nested: + +.. click:: SoftLayer.CLI.virt.list:cli + :prog: vs list + :show-nested: + +.. click:: SoftLayer.CLI.virt.power:pause + :prog: vs pause + :show-nested: + + +.. click:: SoftLayer.CLI.virt.power:power_on + :prog: vs power-on + :show-nested: + + +.. click:: SoftLayer.CLI.virt.power:power_off + :prog: vs power-off + :show-nested: + +.. click:: SoftLayer.CLI.virt.power:resume + :prog: vs resume + :show-nested: + +.. click:: SoftLayer.CLI.virt.power:rescue + :prog: vs rescue + :show-nested: + +.. click:: SoftLayer.CLI.virt.power:reboot + :prog: vs reboot + :show-nested: + +.. click:: SoftLayer.CLI.virt.ready:cli + :prog: vs ready + :show-nested: + +.. click:: SoftLayer.CLI.virt.upgrade:cli + :prog: vs upgrade + :show-nested: + +.. click:: SoftLayer.CLI.virt.usage:cli + :prog: vs usage + :show-nested: + + + + +Reserved Capacity +----------------- +.. toctree:: + :maxdepth: 2 + + vs/reserved_capacity + +Placement Groups +---------------- +.. toctree:: + :maxdepth: 2 + + vs/placement_group - $ slcli vs - Usage: slcli vs [OPTIONS] COMMAND [ARGS]... - - Virtual Servers. - - Options: - --help Show this message and exit. - - Commands: - cancel Cancel virtual servers. - capture Capture SoftLayer image. - create Order/create virtual servers. - create-options Virtual server order options. - credentials List virtual server credentials. - detail Get details for a virtual server. - dns-sync Sync DNS records. - edit Edit a virtual server's details. - list List virtual servers. - network Manage network settings. - pause Pauses an active virtual server. - power_off Power off an active virtual server. - power_on Power on a virtual server. - ready Check if a virtual server is ready. - reboot Reboot an active virtual server. - reload Reload operating system on a virtual server. - rescue Reboot into a rescue image. - resume Resumes a paused virtual server. - upgrade Upgrade a virtual server. diff --git a/docs/cli/vs/placement_group.rst b/docs/cli/vs/placement_group.rst new file mode 100644 index 000000000..c6aa09944 --- /dev/null +++ b/docs/cli/vs/placement_group.rst @@ -0,0 +1,132 @@ +.. _vs_placement_group_user_docs: + +Working with Placement Groups +============================= +A `Placement Group `_ is a way to control which physical servers your virtual servers get provisioned onto. + +To create a `Virtual_PlacementGroup `_ object, you will need to know the following: + +- backendRouterId, from `getAvailableRouters `_ +- ruleId, from `getAllObjects `_ +- name, can be any string, but most be unique on your account + +Once a placement group is created, you can create new virtual servers in that group. Existing VSIs cannot be moved into a placement group. When ordering a VSI in a placement group, make sure to set the `placementGroupId `_ for each guest in your order. + +use the --placementgroup option with `vs create` to specify creating a VSI in a specific group. + +:: + + + $ slcli vs create -H testGroup001 -D test.com -f B1_1X2X25 -d mex01 -o DEBIAN_LATEST --placementgroup testGroup + +Placement groups can only be deleted once all the virtual guests in the group have been reclaimed. + +.. _cli_vs_placementgroup_create: + +vs placementgroup create +------------------------ +This command will create a placement group. + +:: + + $ slcli vs placementgroup create --name testGroup -b bcr02a.dal06 -r SPREAD + +Options +^^^^^^^ +--name TEXT Name for this new placement group. [required] +-b, --backend_router TEXT backendRouter, can be either the hostname or id. [required] +-r, --rule TEXT The keyName or Id of the rule to govern this placement group. [required] + + +.. _cli_vs_placementgroup_create_options: + +vs placementgroup create-options +-------------------------------- +This command will print out the available routers and rule sets for use in creating a placement group. + +:: + + $ slcli vs placementgroup create-options + :.................................................: + : Available Routers : + :..............:..............:...................: + : Datacenter : Hostname : Backend Router Id : + :..............:..............:...................: + : Washington 1 : bcr01.wdc01 : 16358 : + : Tokyo 5 : bcr01a.tok05 : 1587015 : + :..............:..............:...................: + :..............: + : Rules : + :....:.........: + : Id : KeyName : + :....:.........: + : 1 : SPREAD : + :....:.........: + +.. _cli_vs_placementgroup_delete: + +vs placementgroup delete +------------------------ +This command will remove a placement group. The placement group needs to be empty for this command to succeed. + +Options +^^^^^^^ +--purge Delete all guests in this placement group. The group itself can be deleted once all VMs are fully reclaimed + +:: + + $ slcli vs placementgroup delete testGroup + +You can use the flag --purge to auto-cancel all VSIs in a placement group. You will still need to wait for them to be reclaimed before proceeding to delete the group itself. + +:: + + $ slcli vs placementgroup delete testGroup --purge + You are about to delete the following guests! + issues10691547768562.test.com, issues10691547768572.test.com, issues10691547768552.test.com, issues10691548718280.test.com + This action will cancel all guests! Continue? [y/N]: y + Deleting issues10691547768562.test.com... + Deleting issues10691547768572.test.com... + Deleting issues10691547768552.test.com... + Deleting issues10691548718280.test.com... + + +.. _cli_vs_placementgroup_list: + +vs placementgroup list +---------------------- +This command will list all placement groups on your account. + +:: + + $ slcli vs placementgroup list + :..........................................................................................: + : Placement Groups : + :.......:...................:................:........:........:...........................: + : Id : Name : Backend Router : Rule : Guests : Created : + :.......:...................:................:........:........:...........................: + : 31741 : fotest : bcr01a.tor01 : SPREAD : 1 : 2018-11-22T14:36:10-06:00 : + : 64535 : testGroup : bcr01a.mex01 : SPREAD : 3 : 2019-01-17T14:36:42-06:00 : + :.......:...................:................:........:........:...........................: + +.. _cli_vs_placementgroup_detail: + +vs placementgroup detail +------------------------ +This command will provide some detailed information about a specific placement group + +:: + + $ slcli vs placementgroup detail testGroup + :.......:............:................:........:...........................: + : Id : Name : Backend Router : Rule : Created : + :.......:............:................:........:...........................: + : 64535 : testGroup : bcr01a.mex01 : SPREAD : 2019-01-17T14:36:42-06:00 : + :.......:............:................:........:...........................: + :..........:........................:...............:..............:.....:........:...........................:.............: + : Id : FQDN : Primary IP : Backend IP : CPU : Memory : Provisioned : Transaction : + :..........:........................:...............:..............:.....:........:...........................:.............: + : 69134895 : testGroup62.test.com : 169.57.70.166 : 10.131.11.32 : 1 : 1024 : 2019-01-17T17:44:50-06:00 : - : + : 69134901 : testGroup72.test.com : 169.57.70.184 : 10.131.11.59 : 1 : 1024 : 2019-01-17T17:44:53-06:00 : - : + : 69134887 : testGroup52.test.com : 169.57.70.187 : 10.131.11.25 : 1 : 1024 : 2019-01-17T17:44:43-06:00 : - : + :..........:........................:...............:..............:.....:........:...........................:.............: \ No newline at end of file diff --git a/docs/cli/vs/reserved_capacity.rst b/docs/cli/vs/reserved_capacity.rst new file mode 100644 index 000000000..3193febff --- /dev/null +++ b/docs/cli/vs/reserved_capacity.rst @@ -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 --git a/docs/conf.py b/docs/conf.py index 8c885c2d2..9e4c5205f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,8 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', - 'sphinx.ext.viewcode'] + 'sphinx.ext.viewcode', + 'sphinx_click.ext'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -48,7 +49,7 @@ project = u'SoftLayer API Python Client' # Hack to avoid the "Redefining built-in 'copyright'" error from static # analysis tools -globals()['copyright'] = u'2017, SoftLayer Technologies, Inc.' +globals()['copyright'] = u'2019, SoftLayer Technologies, Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/config_file.rst b/docs/config_file.rst index ecea6364d..57ecdc1d1 100644 --- a/docs/config_file.rst +++ b/docs/config_file.rst @@ -25,3 +25,13 @@ automatically by the `slcli setup` command detailed here: api_key = oyVmeipYQCNrjVS4rF9bHWV7D75S6pa1fghFl384v7mwRCbHTfuJ8qRORIqoVnha endpoint_url = https://api.softlayer.com/xmlrpc/v3/ timeout = 40 + + +*Cloud.ibm.com Config Example* +:: + + [softlayer] + username = apikey + api_key = 123cNyhzg45Ab6789ADyzwR_2LAagNVbySgY73tAQOz1 + endpoint_url = https://api.softlayer.com/rest/v3.1/ + timeout = 40 \ No newline at end of file diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 21bb0d403..a0abdcc13 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -87,6 +87,33 @@ is: 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 @@ fabric, use the following commands. cd docs make html + sphinx-build -b html ./ ./html The primary docs are built at `Read the Docs `_. @@ -121,6 +149,17 @@ Flake8, with project-specific exceptions, can be run by using tox: 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 --git a/docs/index.rst b/docs/index.rst index d4bf7dd70..1b3c2b390 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,17 +2,15 @@ SoftLayer API Python Client |version| ======================================== -`API Docs `_ ``|`` +`API Docs `_ ``|`` `GitHub `_ ``|`` `Issues `_ ``|`` `Pull Requests `_ ``|`` `PyPI `_ ``|`` -`Twitter `_ ``|`` -`#softlayer on freenode `_ This is the documentation to SoftLayer's Python API Bindings. These bindings -use SoftLayer's `XML-RPC interface `_ +use SoftLayer's `XML-RPC interface `_ in order to manage SoftLayer services. .. toctree:: @@ -38,10 +36,9 @@ Contributing External Links -------------- -* `SoftLayer API Documentation `_ +* `SoftLayer API Documentation `_ * `Source on GitHub `_ * `Issues `_ * `Pull Requests `_ * `PyPI `_ -* `Twitter `_ -* `#softlayer on freenode `_ + diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..acb2b7258 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +sphinx +sphinx-click +click +prettytable \ No newline at end of file diff --git a/fabfile.py b/fabfile.py index c864c537e..a393fe99b 100644 --- a/fabfile.py +++ b/fabfile.py @@ -1,49 +1,67 @@ +import click import os.path import shutil - -from fabric.api import local, lcd, puts, abort - +import subprocess +import sys +from pprint import pprint as pp def make_html(): - "Build HTML docs" - with lcd('docs'): - local('make html') - + """Build HTML docs""" + click.secho("Building HTML") + subprocess.run('make html', cwd='docs', shell=True) def upload(): - "Upload distribution to PyPi" - local('python setup.py sdist upload') - local('python setup.py bdist_wheel upload') + """Upload distribution to PyPi""" + cmd_setup = 'python setup.py sdist bdist_wheel' + click.secho("\tRunning %s" % cmd_setup, fg='yellow') + subprocess.run(cmd_setup, shell=True) + cmd_twine = 'twine upload dist/*' + click.secho("\tRunning %s" % cmd_twine, fg='yellow') + subprocess.run(cmd_twine, shell=True) def clean(): - puts("* Cleaning Repo") + click.secho("* Cleaning Repo") directories = ['.tox', 'SoftLayer.egg-info', 'build', 'dist'] for directory in directories: if os.path.exists(directory) and os.path.isdir(directory): shutil.rmtree(directory) -def release(version, force=False): +@click.command() +@click.argument('version') +@click.option('--force', default=False, is_flag=True, help="Force upload") +def release(version, force): """Perform a release. Example: - $ fab release:3.0.0 + $ python fabfile.py 1.2.3 """ if version.startswith("v"): - abort("Version should not start with 'v'") + exit("Version should not start with 'v'") version_str = "v%s" % version clean() - local("pip install wheel") + subprocess.run("pip install wheel", shell=True) - puts(" * Uploading to PyPI") + print(" * Uploading to PyPI") upload() + make_html() - puts(" * Tagging Version %s" % version_str) force_option = 'f' if force else '' - local("git tag -%sam \"%s\" %s" % (force_option, version_str, version_str)) + cmd_tag = "git tag -%sam \"%s\" %s" % (force_option, version_str, version_str) + + click.secho(" * Tagging Version %s" % version_str) + click.secho("\tRunning %s" % cmd_tag, fg='yellow') + subprocess.run(cmd_tag, shell=True) + + + cmd_push = "git push upstream %s" % version_str + click.secho(" * Pushing Tag to upstream") + click.secho("\tRunning %s" % cmd_push, fg='yellow') + subprocess.run(cmd_push, shell=True) + - puts(" * Pushing Tag to upstream") - local("git push upstream %s" % version_str) +if __name__ == '__main__': + release() \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index ba4e6f120..4b08cec20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,8 @@ [tool:pytest] python_files = *_tests.py +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning [wheel] universal=1 diff --git a/setup.py b/setup.py index 30c38b966..692ef789d 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='SoftLayer', - version='5.5.1', + version='5.7.2', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.', @@ -32,13 +32,13 @@ install_requires=[ 'six >= 1.7.0', 'ptable >= 0.9.2', - 'click >= 5', - 'requests >= 2.18.4', - 'prompt_toolkit >= 0.53', + 'click >= 7', + 'requests >= 2.20.0', + 'prompt_toolkit >= 2', 'pygments >= 2.0.0', - 'urllib3 >= 1.22' + 'urllib3 >= 1.24' ], - keywords=['softlayer', 'cloud'], + keywords=['softlayer', 'cloud', 'slcli'], classifiers=[ 'Environment :: Console', 'Environment :: Web Environment', diff --git a/snap/README.md b/snap/README.md index 3fb1722a5..12ec05bcc 100644 --- a/snap/README.md +++ b/snap/README.md @@ -10,3 +10,8 @@ Snaps are available for any Linux OS running snapd, the service that runs and ma or to learn to build and publish your own snaps, please see: https://docs.snapcraft.io/build-snaps/languages?_ga=2.49470950.193172077.1519771181-1009549731.1511399964 + +# Releasing +Builds should be automagic here. + +https://build.snapcraft.io/user/softlayer/softlayer-python diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index c05b79e30..d6634a551 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: slcli # check to see if it's available -version: '5.5.1+git' # check versioning +version: '5.7.2+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. diff --git a/tests/CLI/modules/account_tests.py b/tests/CLI/modules/account_tests.py new file mode 100644 index 000000000..c495546c8 --- /dev/null +++ b/tests/CLI/modules/account_tests.py @@ -0,0 +1,95 @@ +""" + SoftLayer.tests.CLI.modules.account_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests for the user cli command +""" +from SoftLayer.fixtures import SoftLayer_Account as SoftLayer_Account +from SoftLayer import testing + + +class AccountCLITests(testing.TestCase): + + def set_up(self): + self.SLNOE = 'SoftLayer_Notification_Occurrence_Event' + + # slcli account event-detail + def test_event_detail(self): + result = self.run_command(['account', 'event-detail', '1234']) + self.assert_no_fail(result) + self.assert_called_with(self.SLNOE, 'getObject', identifier='1234') + + def test_event_details_ack(self): + result = self.run_command(['account', 'event-detail', '1234', '--ack']) + self.assert_no_fail(result) + self.assert_called_with(self.SLNOE, 'getObject', identifier='1234') + self.assert_called_with(self.SLNOE, 'acknowledgeNotification', identifier='1234') + + # slcli account events + def test_events(self): + result = self.run_command(['account', 'events']) + self.assert_no_fail(result) + self.assert_called_with(self.SLNOE, 'getAllObjects') + + def test_event_ack_all(self): + result = self.run_command(['account', 'events', '--ack-all']) + self.assert_no_fail(result) + self.assert_called_with(self.SLNOE, 'getAllObjects') + self.assert_called_with(self.SLNOE, 'acknowledgeNotification', identifier=1234) + + # slcli account invoice-detail + + def test_invoice_detail(self): + result = self.run_command(['account', 'invoice-detail', '1234']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Invoice', 'getInvoiceTopLevelItems', identifier='1234') + + def test_invoice_detail_details(self): + result = self.run_command(['account', 'invoice-detail', '1234', '--details']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Invoice', 'getInvoiceTopLevelItems', identifier='1234') + + # slcli account invoices + def test_invoices(self): + result = self.run_command(['account', 'invoices']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getInvoices', limit=50) + + def test_invoices_limited(self): + result = self.run_command(['account', 'invoices', '--limit=10']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getInvoices', limit=10) + + def test_invoices_closed(self): + _filter = { + 'invoices': { + 'createDate': { + 'operation': 'orderBy', + 'options': [{ + 'name': 'sort', + 'value': ['DESC'] + }] + } + } + } + result = self.run_command(['account', 'invoices', '--closed']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getInvoices', limit=50, filter=_filter) + + def test_invoices_all(self): + result = self.run_command(['account', 'invoices', '--all']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getInvoices', limit=50) + + def test_single_invoice(self): + amock = self.set_mock('SoftLayer_Account', 'getInvoices') + amock.return_value = SoftLayer_Account.getInvoices[0] + result = self.run_command(['account', 'invoices', '--limit=1']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getInvoices', limit=1) + + # slcli account summary + def test_account_summary(self): + result = self.run_command(['account', 'summary']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getObject') diff --git a/tests/CLI/modules/cdn_tests.py b/tests/CLI/modules/cdn_tests.py index d15259ab5..cb3c59e43 100644 --- a/tests/CLI/modules/cdn_tests.py +++ b/tests/CLI/modules/cdn_tests.py @@ -4,10 +4,11 @@ :license: MIT, see LICENSE for more details. """ -from SoftLayer import testing - import json +from SoftLayer.CLI import exceptions +from SoftLayer import testing + class CdnTests(testing.TestCase): @@ -16,67 +17,81 @@ def test_list_accounts(self): self.assert_no_fail(result) self.assertEqual(json.loads(result.output), - [{'notes': None, - 'created': '2012-06-25T14:05:28-07:00', - 'type': 'ORIGIN_PULL', - 'id': 1234, - 'account_name': '1234a'}, - {'notes': None, - 'created': '2012-07-24T13:34:25-07:00', - 'type': 'POP_PULL', - 'id': 1234, - 'account_name': '1234a'}]) + [{'cname': 'cdnakauuiet7s6u6.cdnedge.bluemix.net', + 'domain': 'test.example.com', + 'origin': '1.1.1.1', + 'status': 'CNAME_CONFIGURATION', + 'unique_id': '9934111111111', + 'vendor': 'akamai'}] + ) def test_detail_account(self): - result = self.run_command(['cdn', 'detail', '1245']) + result = self.run_command(['cdn', 'detail', '--history=30', '1245']) self.assert_no_fail(result) self.assertEqual(json.loads(result.output), - {'notes': None, - 'created': '2012-06-25T14:05:28-07:00', - 'type': 'ORIGIN_PULL', - 'status': 'ACTIVE', - 'id': 1234, - 'account_name': '1234a'}) - - def test_load_content(self): - result = self.run_command(['cdn', 'load', '1234', - 'http://example.com']) - - self.assert_no_fail(result) - self.assertEqual(result.output, "") + {'hit_radio': '0.0 %', + 'hostname': 'test.example.com', + 'origin': '1.1.1.1', + 'origin_type': 'HOST_SERVER', + 'path': '/', + 'protocol': 'HTTP', + 'provider': 'akamai', + 'status': 'CNAME_CONFIGURATION', + 'total_bandwidth': '0.0 GB', + 'total_hits': '0', + 'unique_id': '9934111111111'} + ) def test_purge_content(self): result = self.run_command(['cdn', 'purge', '1234', - 'http://example.com']) + '/article/file.txt']) self.assert_no_fail(result) - self.assertEqual(result.output, "") def test_list_origins(self): result = self.run_command(['cdn', 'origin-list', '1234']) self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), [ - {'media_type': 'FLASH', - 'origin_url': 'http://ams01.objectstorage.softlayer.net:80', - 'cname': None, - 'id': '12345'}, - {'media_type': 'FLASH', - 'origin_url': 'http://sng01.objectstorage.softlayer.net:80', - 'cname': None, - 'id': '12345'}]) - - def test_add_origin(self): - result = self.run_command(['cdn', 'origin-add', '1234', - 'http://example.com']) + self.assertEqual(json.loads(result.output), [{'HTTP Port': 80, + 'Origin': '10.10.10.1', + 'Path': '/example', + 'Status': 'RUNNING'}, + {'HTTP Port': 80, + 'Origin': '10.10.10.1', + 'Path': '/example1', + 'Status': 'RUNNING'}]) + + def test_add_origin_server(self): + result = self.run_command( + ['cdn', 'origin-add', '-t', 'server', '-H=test.example.com', '-p', 80, '-o', 'web', '-c=include-all', + '1234', '10.10.10.1', '/example/videos2']) + + self.assert_no_fail(result) + + def test_add_origin_storage(self): + result = self.run_command(['cdn', 'origin-add', '-t', 'storage', '-b=test-bucket', '-H=test.example.com', + '-p', 80, '-o', 'web', '-c=include-all', '1234', '10.10.10.1', '/example/videos2']) + + self.assert_no_fail(result) + + def test_add_origin_without_storage(self): + result = self.run_command(['cdn', 'origin-add', '-t', 'storage', '-H=test.example.com', '-p', 80, + '-o', 'web', '-c=include-all', '1234', '10.10.10.1', '/example/videos2']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.ArgumentError) + + def test_add_origin_storage_with_file_extensions(self): + result = self.run_command( + ['cdn', 'origin-add', '-t', 'storage', '-b=test-bucket', '-e', 'jpg', '-H=test.example.com', '-p', 80, + '-o', 'web', '-c=include-all', '1234', '10.10.10.1', '/example/videos2']) self.assert_no_fail(result) - self.assertEqual(result.output, "") def test_remove_origin(self): result = self.run_command(['cdn', 'origin-remove', '1234', - 'http://example.com']) + '/example1']) self.assert_no_fail(result) - self.assertEqual(result.output, "") + self.assertEqual(result.output, "Origin with path /example1 has been deleted\n") diff --git a/tests/CLI/modules/config_tests.py b/tests/CLI/modules/config_tests.py index 5c21b1da8..ec018a53c 100644 --- a/tests/CLI/modules/config_tests.py +++ b/tests/CLI/modules/config_tests.py @@ -51,10 +51,12 @@ def set_up(self): transport = testing.MockableTransport(SoftLayer.FixtureTransport()) self.env.client = SoftLayer.BaseClient(transport=transport) + @mock.patch('SoftLayer.Client') @mock.patch('SoftLayer.CLI.formatting.confirm') @mock.patch('SoftLayer.CLI.environment.Environment.getpass') @mock.patch('SoftLayer.CLI.environment.Environment.input') - def test_setup(self, mocked_input, getpass, confirm_mock): + def test_setup(self, mocked_input, getpass, confirm_mock, client): + client.return_value = self.env.client if(sys.platform.startswith("win")): self.skipTest("Test doesn't work in Windows") with tempfile.NamedTemporaryFile() as config_file: @@ -62,24 +64,23 @@ def test_setup(self, mocked_input, getpass, confirm_mock): getpass.return_value = 'A' * 64 mocked_input.side_effect = ['user', 'public', 0] - result = self.run_command(['--config=%s' % config_file.name, - 'config', 'setup']) + result = self.run_command(['--config=%s' % config_file.name, 'config', 'setup']) self.assert_no_fail(result) - self.assertTrue('Configuration Updated Successfully' - in result.output) + self.assertTrue('Configuration Updated Successfully' in result.output) contents = config_file.read().decode("utf-8") + self.assertTrue('[softlayer]' in contents) self.assertTrue('username = user' in contents) - self.assertTrue('api_key = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA' in contents) - self.assertTrue('endpoint_url = %s' % consts.API_PUBLIC_ENDPOINT - in contents) + self.assertTrue('api_key = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' in contents) + self.assertTrue('endpoint_url = %s' % consts.API_PUBLIC_ENDPOINT in contents) + @mock.patch('SoftLayer.Client') @mock.patch('SoftLayer.CLI.formatting.confirm') @mock.patch('SoftLayer.CLI.environment.Environment.getpass') @mock.patch('SoftLayer.CLI.environment.Environment.input') - def test_setup_cancel(self, mocked_input, getpass, confirm_mock): + def test_setup_cancel(self, mocked_input, getpass, confirm_mock, client): + client.return_value = self.env.client with tempfile.NamedTemporaryFile() as config_file: confirm_mock.return_value = False getpass.return_value = 'A' * 64 @@ -115,6 +116,17 @@ def test_get_user_input_custom(self, mocked_input, getpass): self.assertEqual(endpoint_url, 'custom-endpoint') + @mock.patch('SoftLayer.CLI.environment.Environment.getpass') + @mock.patch('SoftLayer.CLI.environment.Environment.input') + def test_github_1074(self, mocked_input, getpass): + """Tests to make sure directly using an endpoint works""" + getpass.return_value = 'A' * 64 + mocked_input.side_effect = ['user', 'test-endpoint', 0] + + _, _, endpoint_url, _ = config.get_user_input(self.env) + + self.assertEqual(endpoint_url, 'test-endpoint') + @mock.patch('SoftLayer.CLI.environment.Environment.getpass') @mock.patch('SoftLayer.CLI.environment.Environment.input') def test_get_user_input_default(self, mocked_input, getpass): diff --git a/tests/CLI/modules/dedicatedhost_tests.py b/tests/CLI/modules/dedicatedhost_tests.py index ead835261..d20ce5355 100644 --- a/tests/CLI/modules/dedicatedhost_tests.py +++ b/tests/CLI/modules/dedicatedhost_tests.py @@ -6,7 +6,6 @@ """ import json import mock -import os import SoftLayer from SoftLayer.CLI import exceptions @@ -29,21 +28,17 @@ def test_list_dedicated_hosts(self): '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,20 @@ def test_details(self): '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, + 'tags': None} ) def test_details_no_owner(self): @@ -85,18 +81,19 @@ def test_details_no_owner(self): '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, + 'tags': None} ) def test_create_options(self): @@ -137,16 +134,16 @@ def test_create_options_get_routers(self): 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' } ]] ) @@ -159,32 +156,66 @@ def test_create(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dedicatedhost', 'create', - '--hostname=host', - '--domain=example.com', + '--hostnames=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', + '--hostnames=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 +230,8 @@ def test_create_verify(self): result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostnames=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=hourly']) @@ -210,12 +241,12 @@ def test_create_verify(self): 'useHourlyPricing': True, 'hardware': [{ - 'hostname': 'host', - 'domain': 'example.com', + 'hostname': 'test-dedicated', + 'domain': 'test.com', 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } } }], @@ -229,8 +260,8 @@ def test_create_verify(self): result = self.run_command(['dh', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostnames=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=monthly']) @@ -239,12 +270,12 @@ def test_create_verify(self): 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 +293,8 @@ def test_create_aborted(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dh', 'create', - '--hostname=host', - '--domain=example.com', + '--hostnames=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=monthly']) @@ -271,23 +302,6 @@ def test_create_aborted(self): 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 +312,97 @@ def test_create_verify_no_price_or_more_than_one(self): result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostnames=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 --git a/tests/CLI/modules/dns_tests.py b/tests/CLI/modules/dns_tests.py index 836da74a9..3c1329b39 100644 --- a/tests/CLI/modules/dns_tests.py +++ b/tests/CLI/modules/dns_tests.py @@ -72,11 +72,41 @@ def test_list_records(self): '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 --git a/tests/CLI/modules/event_log_tests.py b/tests/CLI/modules/event_log_tests.py new file mode 100644 index 000000000..d22847317 --- /dev/null +++ b/tests/CLI/modules/event_log_tests.py @@ -0,0 +1,93 @@ +""" + SoftLayer.tests.CLI.modules.event_log_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + :license: MIT, see LICENSE for more details. +""" + +import json + +from SoftLayer import testing + + +class EventLogTests(testing.TestCase): + + def test_get_event_log_with_metadata(self): + result = self.run_command(['event-log', 'get', '--metadata']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects') + self.assertIn('Metadata', result.output) + + def test_get_event_log_without_metadata(self): + result = self.run_command(['event-log', 'get', '--no-metadata']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects') + self.assert_called_with('SoftLayer_User_Customer', 'getObject', identifier=400) + self.assertNotIn('Metadata', result.output) + + def test_get_event_log_empty(self): + mock = self.set_mock('SoftLayer_Event_Log', 'getAllObjects') + mock.return_value = None + + result = self.run_command(['event-log', 'get']) + expected = 'Event, Object, Type, Date, Username\n' \ + 'No logs available for filter {}.\n' + self.assert_no_fail(result) + self.assertEqual(expected, result.output) + + def test_get_event_log_over_limit(self): + result = self.run_command(['event-log', 'get', '-l 1']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects') + self.assertEqual(2, result.output.count("\n")) + + def test_get_event_log_types(self): + expected = [ + { + "types": {"value": "Account"} + }, + { + "types": {"value": "CDN"} + }, + { + "types": {"value": "User"} + }, + { + "types": {"value": "Bare Metal Instance"} + }, + { + "types": {"value": "API Authentication"} + }, + { + "types": {"value": "Server"} + }, + { + "types": {"value": "CCI"} + }, + { + "types": {"value": "Image"} + }, + { + "types": {"value": "Bluemix LB"} + }, + { + "types": {"value": "Facility"} + }, + { + "types": {"value": "Cloud Object Storage"} + }, + { + "types": {"value": "Security Group"} + } + ] + + result = self.run_command(['event-log', 'types']) + + self.assert_no_fail(result) + self.assertEqual(expected, json.loads(result.output)) + + def test_get_unlimited_events(self): + result = self.run_command(['event-log', 'get', '-l -1']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects') + self.assertEqual(8, result.output.count("\n")) diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py index 9bc56320b..465e9ec03 100644 --- a/tests/CLI/modules/file_tests.py +++ b/tests/CLI/modules/file_tests.py @@ -4,6 +4,7 @@ :license: MIT, see LICENSE for more details. """ +from SoftLayer import exceptions from SoftLayer import testing import json @@ -94,6 +95,31 @@ def test_volume_cancel(self): self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', args=(False, True, None)) + def test_volume_cancel_with_billing_item(self): + result = self.run_command([ + '--really', 'file', 'volume-cancel', '1234']) + + self.assert_no_fail(result) + self.assertEqual('File volume with id 1234 has been marked' + ' for cancellation\n', result.output) + self.assert_called_with('SoftLayer_Network_Storage', 'getObject') + + def test_volume_cancel_without_billing_item(self): + p_mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + p_mock.return_value = { + "accountId": 1234, + "capacityGb": 20, + "createDate": "2015-04-29T06:55:55-07:00", + "id": 11111, + "nasType": "NAS", + "username": "SL01SEV307608_1" + } + + result = self.run_command([ + '--really', 'file', 'volume-cancel', '1234']) + + self.assertIsInstance(result.exception, exceptions.SoftLayerError) + def test_volume_detail(self): result = self.run_command(['file', 'volume-detail', '1234']) diff --git a/tests/CLI/modules/object_storage_tests.py b/tests/CLI/modules/object_storage_tests.py index 0f59c847e..74d70152e 100644 --- a/tests/CLI/modules/object_storage_tests.py +++ b/tests/CLI/modules/object_storage_tests.py @@ -16,8 +16,9 @@ def test_list_accounts(self): self.assert_no_fail(result) self.assertEqual(json.loads(result.output), - [{'id': 12345, 'name': 'SLOS12345-1'}, - {'id': 12346, 'name': 'SLOS12345-2'}]) + [{'apiType': 'S3', 'id': 12345, 'name': 'SLOS12345-1'}, + {'apiType': 'Swift', 'id': 12346, 'name': 'SLOS12345-2'}] + ) def test_list_endpoints(self): accounts = self.set_mock('SoftLayer_Account', 'getHubNetworkStorage') @@ -36,3 +37,70 @@ def test_list_endpoints(self): [{'datacenter': 'dal05', 'private': 'https://dal05/auth/v1.0/', 'public': 'https://dal05/auth/v1.0/'}]) + + def test_create_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'credentialCreate') + accounts.return_value = { + "accountId": "12345", + "createDate": "2019-04-05T13:25:25-06:00", + "id": 11111, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCy", + "username": "XfHhBNBPlPdl", + "type": { + "description": "A credential for generating S3 Compatible Signatures.", + "keyName": "S3_COMPATIBLE_SIGNATURE", + "name": "S3 Compatible Signature" + } + } + + result = self.run_command(['object-storage', 'credential', 'create', '100']) + + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), + [{'id': 11111, + 'password': 'nwUEUsx6PiEoN0B1Xe9z9hUCy', + 'type_name': 'S3 Compatible Signature', + 'username': 'XfHhBNBPlPdl'}] + ) + + def test_delete_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'credentialDelete') + accounts.return_value = True + + result = self.run_command(['object-storage', 'credential', 'delete', '-c', 100, '100']) + + self.assert_no_fail(result) + self.assertEqual(result.output, 'True\n') + + def test_limit_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'getCredentialLimit') + accounts.return_value = 2 + + result = self.run_command(['object-storage', 'credential', 'limit', '100']) + + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), [{'limit': 2}]) + + def test_list_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'getCredentials') + accounts.return_value = [{'id': 1103123, + 'password': 'nwUEUsx6PiEoN0B1Xe9z9hUCyXM', + 'type': {'name': 'S3 Compatible Signature'}, + 'username': 'XfHhBNBPlPdlWya'}, + {'id': 1103333, + 'password': 'nwUEUsx6PiEoN0B1Xe9z9', + 'type': {'name': 'S3 Compatible Signature'}, + 'username': 'XfHhBNBPlPd'}] + + result = self.run_command(['object-storage', 'credential', 'list', '100']) + + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output), + [{'id': 1103123, + 'password': 'nwUEUsx6PiEoN0B1Xe9z9hUCyXM', + 'type_name': 'S3 Compatible Signature', + 'username': 'XfHhBNBPlPdlWya'}, + {'id': 1103333, + 'password': 'nwUEUsx6PiEoN0B1Xe9z9', + 'type_name': 'S3 Compatible Signature', + 'username': 'XfHhBNBPlPd'}]) diff --git a/tests/CLI/modules/order_tests.py b/tests/CLI/modules/order_tests.py index b48cb8a53..a82b731fc 100644 --- a/tests/CLI/modules/order_tests.py +++ b/tests/CLI/modules/order_tests.py @@ -4,7 +4,10 @@ :license: MIT, see LICENSE for more details. """ import json +import sys +import tempfile +from SoftLayer.CLI import exceptions from SoftLayer import testing @@ -38,35 +41,26 @@ def test_item_list(self): self.assert_no_fail(result) self.assert_called_with('SoftLayer_Product_Package', 'getItems') - expected_results = [{'category': 'testing', - 'keyName': 'item1', - 'description': 'description1'}, - {'category': 'testing', - 'keyName': 'item2', - 'description': 'description2'}] - self.assertEqual(expected_results, json.loads(result.output)) + self.assertIn('description2', result.output) + self.assertIn('testing', result.output) + self.assertIn('item2', 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 +68,21 @@ def test_package_list_keyword(self): 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 +106,70 @@ def test_place(self): 'status': 'APPROVED'}, json.loads(result.output)) + def test_place_with_quantity(self): + order_date = '2017-04-04 07:39:20' + order = {'orderId': 1234, 'orderDate': order_date, 'placedOrder': {'status': 'APPROVED'}} + verify_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + place_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + items_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + + verify_mock.return_value = self._get_verified_order_return() + place_mock.return_value = order + items_mock.return_value = self._get_order_items() + + result = self.run_command(['-y', 'order', 'place', '--quantity=2', 'package', 'DALLAS13', 'ITEM1', + '--complex-type', 'SoftLayer_Container_Product_Order_Thing']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + self.assertEqual({'id': 1234, + 'created': order_date, + 'status': 'APPROVED'}, + json.loads(result.output)) + + def test_place_extras_parameter_fail(self): + result = self.run_command(['-y', 'order', 'place', 'package', 'DALLAS13', 'ITEM1', + '--extras', '{"device":[']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + 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_place_quote_extras_parameter_fail(self): + result = self.run_command(['-y', 'order', 'place-quote', 'package', 'DALLAS13', 'ITEM1', + '--extras', '{"device":[']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + def test_verify_hourly(self): order_date = '2017-04-04 07:39:20' order = {'orderId': 1234, 'orderDate': order_date, @@ -198,6 +254,12 @@ def test_preset_list(self): 'description': 'description3'}], json.loads(result.output)) + def test_preset_list_keywork(self): + result = self.run_command(['order', 'preset-list', 'package', '--keyword', 'testKeyWord']) + _filter = {'activePresets': {'name': {'operation': '*= testKeyWord'}}} + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Package', 'getActivePresets', filter=_filter) + def test_location_list(self): result = self.run_command(['order', 'package-locations', 'package']) self.assert_no_fail(result) @@ -208,6 +270,102 @@ def test_location_list(self): print(result.output) self.assertEqual(expected_results, json.loads(result.output)) + def test_quote_verify(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + + def test_quote_verify_image(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--image', '1234', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_Guest_Block_Device_Template_Group', 'getObject', identifier='1234') + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual('0B5DEAF4-643D-46CA-A695-CECBE8832C9D', verify_args['imageTemplateGlobalIdentifier']) + + def test_quote_verify_image_guid(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--image', + '0B5DEAF4-643D-46CA-A695-CECBE8832C9D', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual('0B5DEAF4-643D-46CA-A695-CECBE8832C9D', verify_args['imageTemplateGlobalIdentifier']) + + def test_quote_verify_userdata(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--userdata', 'aaaa1234', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual([{'value': 'aaaa1234'}], verify_args['hardware'][0]['userData']) + + def test_quote_verify_userdata_file(self): + if (sys.platform.startswith("win")): + self.skipTest("TempFile tests doesn't work in Windows") + with tempfile.NamedTemporaryFile() as userfile: + userfile.write(b"some data") + userfile.flush() + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--userfile', userfile.name, + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual([{'value': 'some data'}], verify_args['hardware'][0]['userData']) + + def test_quote_verify_sshkey(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--key', 'Test 1', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + + self.assert_called_with('SoftLayer_Account', 'getSshKeys') + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual(['100'], verify_args['sshKeys']) + + def test_quote_verify_postinstall_others(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--quantity', '2', + '--postinstall', 'https://127.0.0.1/test.sh', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual(['https://127.0.0.1/test.sh'], verify_args['provisionScripts']) + self.assertEqual(2, verify_args['quantity']) + + def test_quote_place(self): + result = self.run_command([ + 'order', 'quote', '12345', '--fqdn', 'test01.test.com', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'placeOrder', identifier='12345') + + def test_quote_detail(self): + result = self.run_command(['order', 'quote-detail', '12345']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getObject', identifier='12345') + + def test_quote_list(self): + result = self.run_command(['order', 'quote-list']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getActiveQuotes') + def _get_order_items(self): item1 = {'keyName': 'ITEM1', 'description': 'description1', 'itemCategory': {'categoryCode': 'cat1'}, @@ -227,3 +385,13 @@ def _get_verified_order_return(self): price2 = {'item': item2, 'hourlyRecurringFee': '0.05', 'recurringFee': '150'} 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 --git a/tests/CLI/modules/report_tests.py b/tests/CLI/modules/report_tests.py index f57d87120..3d580edad 100644 --- a/tests/CLI/modules/report_tests.py +++ b/tests/CLI/modules/report_tests.py @@ -96,7 +96,7 @@ def test_bandwidth_report(self): stripped_output = '[' + result.output.split('[', 1)[1] self.assertEqual([ { - 'name': 'pool1', + 'hostname': 'pool1', 'pool': None, 'private_in': 30, 'private_out': 40, @@ -104,7 +104,7 @@ def test_bandwidth_report(self): 'public_out': 20, 'type': 'pool', }, { - 'name': 'pool3', + 'hostname': 'pool3', 'pool': None, 'private_in': 30, 'private_out': 40, @@ -112,7 +112,7 @@ def test_bandwidth_report(self): 'public_out': 20, 'type': 'pool', }, { - 'name': 'host1', + 'hostname': 'host1', 'pool': None, 'private_in': 30, 'private_out': 40, @@ -120,7 +120,7 @@ def test_bandwidth_report(self): 'public_out': 20, 'type': 'virtual', }, { - 'name': 'host3', + 'hostname': 'host3', 'pool': 2, 'private_in': 30, 'private_out': 40, @@ -128,7 +128,7 @@ def test_bandwidth_report(self): 'public_out': 20, 'type': 'virtual', }, { - 'name': 'host1', + 'hostname': 'host1', 'pool': None, 'private_in': 30, 'private_out': 40, @@ -136,7 +136,7 @@ def test_bandwidth_report(self): 'public_out': 20, 'type': 'hardware', }, { - 'name': 'host3', + 'hostname': 'host3', 'pool': None, 'private_in': 30, 'private_out': 40, diff --git a/tests/CLI/modules/securitygroup_tests.py b/tests/CLI/modules/securitygroup_tests.py index 65c496b63..b6801fcc8 100644 --- a/tests/CLI/modules/securitygroup_tests.py +++ b/tests/CLI/modules/securitygroup_tests.py @@ -4,11 +4,16 @@ :license: MIT, see LICENSE for more details. """ import json +import mock +import SoftLayer from SoftLayer import testing class SecurityGroupTests(testing.TestCase): + def set_up(self): + self.network = SoftLayer.NetworkManager(self.client) + def test_list_securitygroup(self): result = self.run_command(['sg', 'list']) @@ -118,18 +123,30 @@ def test_securitygroup_rule_list(self): 'remoteGroupId': None, 'protocol': None, 'portRangeMin': None, - 'portRangeMax': None}], + 'portRangeMax': None, + 'createDate': None, + 'modifyDate': None}], json.loads(result.output)) def test_securitygroup_rule_add(self): result = self.run_command(['sg', 'rule-add', '100', '--direction=ingress']) + json.loads(result.output) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Network_SecurityGroup', 'addRules', identifier='100', args=([{'direction': 'ingress'}],)) + self.assertEqual([{"requestId": "addRules", + "rules": "[{'direction': 'ingress', " + "'portRangeMax': '', " + "'portRangeMin': '', " + "'ethertype': 'IPv4', " + "'securityGroupId': 100, 'remoteGroupId': '', " + "'id': 100}]"}], json.loads(result.output)) + def test_securitygroup_rule_add_fail(self): fixture = self.set_mock('SoftLayer_Network_SecurityGroup', 'addRules') fixture.return_value = False @@ -149,6 +166,8 @@ def test_securitygroup_rule_edit(self): args=([{'id': '520', 'direction': 'ingress'}],)) + self.assertEqual([{'requestId': 'editRules'}], json.loads(result.output)) + def test_securitygroup_rule_edit_fail(self): fixture = self.set_mock('SoftLayer_Network_SecurityGroup', 'editRules') fixture.return_value = False @@ -166,6 +185,8 @@ def test_securitygroup_rule_remove(self): 'removeRules', identifier='100', args=(['520'],)) + self.assertEqual([{'requestId': 'removeRules'}], json.loads(result.output)) + def test_securitygroup_rule_remove_fail(self): fixture = self.set_mock('SoftLayer_Network_SecurityGroup', 'removeRules') @@ -203,6 +224,8 @@ def test_securitygroup_interface_add(self): identifier='100', args=(['1000'],)) + self.assertEqual([{'requestId': 'interfaceAdd'}], json.loads(result.output)) + def test_securitygroup_interface_add_fail(self): fixture = self.set_mock('SoftLayer_Network_SecurityGroup', 'attachNetworkComponents') @@ -223,6 +246,8 @@ def test_securitygroup_interface_remove(self): identifier='100', args=(['500'],)) + self.assertEqual([{'requestId': 'interfaceRemove'}], json.loads(result.output)) + def test_securitygroup_interface_remove_fail(self): fixture = self.set_mock('SoftLayer_Network_SecurityGroup', 'detachNetworkComponents') @@ -232,3 +257,85 @@ def test_securitygroup_interface_remove_fail(self): '--network-component=500']) self.assertEqual(result.exit_code, 2) + + @mock.patch('SoftLayer.NetworkManager.get_event_logs_by_request_id') + def test_securitygroup_get_by_request_id(self, event_mock): + event_mock.return_value = [ + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T09:40:32.238869-05:00', + 'eventName': 'Security Group Added', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '{"securityGroupId":"200",' + '"securityGroupName":"test_SG",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public",' + '"requestId":"96c9b47b9e102d2e1d81fba"}', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '59e767e03a57e', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:42:13.089536-05:00', + 'eventName': 'Security Group Rule(s) Removed', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"96c9b47b9e102d2e1d81fba",' + '"rules":[{"ruleId":"800",' + '"remoteIp":null,"remoteGroupId":null,"direction":"ingress",' + '"ethertype":"IPv4",' + '"portRangeMin":2000,"portRangeMax":2001,"protocol":"tcp"}]}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e7765515e28', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + } + ] + + expected = [ + { + 'date': '2017-10-18T09:40:32.238869-05:00', + 'event': 'Security Group Added', + 'label': 'test.softlayer.com', + 'metadata': json.dumps(json.loads( + '{"networkComponentId": "100",' + '"networkInterfaceType": "public",' + '"requestId": "96c9b47b9e102d2e1d81fba",' + '"securityGroupId": "200",' + '"securityGroupName": "test_SG"}' + ), + indent=4, + sort_keys=True + ) + }, + { + 'date': '2017-10-18T10:42:13.089536-05:00', + 'event': 'Security Group Rule(s) Removed', + 'label': 'test_SG', + 'metadata': json.dumps(json.loads( + '{"requestId": "96c9b47b9e102d2e1d81fba",' + '"rules": [{"direction": "ingress",' + '"ethertype": "IPv4",' + '"portRangeMax": 2001,' + '"portRangeMin": 2000,' + '"protocol": "tcp",' + '"remoteGroupId": null,' + '"remoteIp": null,' + '"ruleId": "800"}]}' + ), + indent=4, + sort_keys=True + ) + } + ] + + result = self.run_command(['sg', 'event-log', '96c9b47b9e102d2e1d81fba']) + + self.assertEqual(expected, json.loads(result.output)) diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index 2f26c4ec6..61ab12a20 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -96,37 +96,17 @@ def test_server_credentials_exception_password_not_found(self): ) def test_server_details(self): - result = self.run_command(['server', 'detail', '1234', - '--passwords', '--price']) - expected = { - 'cores': 2, - 'created': '2013-08-01 15:23:45', - 'datacenter': 'TEST00', - 'guid': '1a2b3c-1701', - 'domain': 'test.sftlyr.ws', - 'hostname': 'hardware-test1', - 'fqdn': 'hardware-test1.test.sftlyr.ws', - 'id': 1000, - 'ipmi_ip': '10.1.0.3', - 'memory': 2048, - 'notes': 'These are test notes.', - 'os': 'Ubuntu', - 'os_version': 'Ubuntu 12.04 LTS', - 'owner': 'chechu', - 'prices': [{'Item': 'Total', 'Recurring Price': 16.08}, - {'Item': 'test', 'Recurring Price': 1}], - 'private_ip': '10.1.0.2', - 'public_ip': '172.16.1.100', - 'remote users': [{'password': 'abc123', 'ipmi_username': 'root'}], - 'status': 'ACTIVE', - 'tags': ['test_tag'], - 'users': [{'password': 'abc123', 'username': 'root'}], - 'vlans': [{'id': 9653, 'number': 1800, 'type': 'PRIVATE'}, - {'id': 19082, 'number': 3672, 'type': 'PUBLIC'}] - } + result = self.run_command(['server', 'detail', '1234', '--passwords', '--price']) self.assert_no_fail(result) - self.assertEqual(expected, json.loads(result.output)) + output = json.loads(result.output) + self.assertEqual(output['notes'], 'These are test notes.') + self.assertEqual(output['prices'][0]['Recurring Price'], 16.08) + self.assertEqual(output['remote users'][0]['password'], 'abc123') + self.assertEqual(output['users'][0]['username'], 'root') + self.assertEqual(output['vlans'][0]['number'], 1800) + self.assertEqual(output['owner'], 'chechu') + self.assertEqual(output['Bandwidth'][0]['Allotment'], '250') def test_detail_vs_empty_tag(self): mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') @@ -313,7 +293,7 @@ def test_create_server_test_flag(self, verify_mock): result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--port-speed=100', @@ -352,7 +332,7 @@ def test_create_server(self, order_mock): result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--port-speed=100', @@ -370,7 +350,7 @@ def test_create_server_missing_required(self): # This is missing a required argument result = self.run_command(['server', 'create', # Note: no chassis id - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--network=100', @@ -386,7 +366,7 @@ def test_create_server_with_export(self, export_mock): self.skipTest("Test doesn't work in Windows") result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--port-speed=100', @@ -403,7 +383,7 @@ def test_create_server_with_export(self, export_mock): 'datacenter': 'TEST00', 'domain': 'example.com', 'extra': (), - 'hostname': 'test', + 'hostnames': 'test', 'key': (), 'os': 'UBUNTU_12_64', 'port_speed': 100, @@ -412,7 +392,11 @@ def test_create_server_with_export(self, export_mock): 'test': False, 'no_public': True, 'wait': None, - 'template': None}, + 'template': None, + 'output_json': False, + 'subnet_private': None, + 'vlan_private': None, + 'quantity': 1,}, exclude=['wait', 'test']) def test_edit_server_userdata_and_file(self): @@ -482,6 +466,17 @@ def test_update_firmware(self, confirm_mock): 'createFirmwareUpdateTransaction', args=((1, 1, 1, 1)), identifier=1000) + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_reflash_firmware(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['server', 'reflash-firmware', '1000']) + + self.assert_no_fail(result) + self.assertEqual(result.output, "") + self.assert_called_with('SoftLayer_Hardware_Server', + 'createFirmwareReflashTransaction', + args=((1, 1, 1)), identifier=1000) + def test_edit(self): result = self.run_command(['server', 'edit', '--domain=example.com', @@ -580,3 +575,59 @@ def test_going_ready(self, _sleep): result = self.run_command(['hw', 'ready', '100', '--wait=100']) self.assert_no_fail(result) self.assertEqual(result.output, '"READY"\n') + + def test_toggle_ipmi_on(self): + mock.return_value = True + result = self.run_command(['server', 'toggle-ipmi', '--enable', '12345']) + self.assert_no_fail(result) + self.assertEqual(result.output, 'True\n') + + def test_toggle_ipmi_off(self): + mock.return_value = True + result = self.run_command(['server', 'toggle-ipmi', '--disable', '12345']) + self.assert_no_fail(result) + self.assertEqual(result.output, 'True\n') + + def test_bandwidth_hw(self): + if sys.version_info < (3, 6): + self.skipTest("Test requires python 3.6+") + result = self.run_command(['server', 'bandwidth', '100', '--start_date=2019-01-01', '--end_date=2019-02-01']) + self.assert_no_fail(result) + + date = '2019-05-20 23:00' + # number of characters from the end of output to break so json can parse properly + pivot = 157 + # only pyhon 3.7 supports the timezone format slapi uses + if sys.version_info < (3, 7): + date = '2019-05-20T23:00:00-06:00' + pivot = 166 + # Since this is 2 tables, it gets returned as invalid json like "[{}][{}]"" instead of "[[{}],[{}]]" + # so we just do some hacky string substitution to pull out the respective arrays that can be jsonifyied + + output_summary = json.loads(result.output[0:-pivot]) + output_list = json.loads(result.output[-pivot:]) + + self.assertEqual(output_summary[0]['Average MBps'], 0.3841) + self.assertEqual(output_summary[1]['Max Date'], date) + self.assertEqual(output_summary[2]['Max GB'], 0.1172) + self.assertEqual(output_summary[3]['Sum GB'], 0.0009) + + self.assertEqual(output_list[0]['Date'], date) + self.assertEqual(output_list[0]['Pub In'], 1.3503) + + def test_bandwidth_hw_quite(self): + result = self.run_command(['server', 'bandwidth', '100', '--start_date=2019-01-01', + '--end_date=2019-02-01', '-q']) + self.assert_no_fail(result) + date = '2019-05-20 23:00' + + # only pyhon 3.7 supports the timezone format slapi uses + if sys.version_info < (3, 7): + date = '2019-05-20T23:00:00-06:00' + + output_summary = json.loads(result.output) + + self.assertEqual(output_summary[0]['Average MBps'], 0.3841) + self.assertEqual(output_summary[1]['Max Date'], date) + self.assertEqual(output_summary[2]['Max GB'], 0.1172) + self.assertEqual(output_summary[3]['Sum GB'], 0.0009) diff --git a/tests/CLI/modules/sshkey_tests.py b/tests/CLI/modules/sshkey_tests.py index 253309c08..3fea5ce68 100644 --- a/tests/CLI/modules/sshkey_tests.py +++ b/tests/CLI/modules/sshkey_tests.py @@ -41,7 +41,7 @@ def test_add_by_option(self): self.assert_no_fail(result) self.assertEqual(json.loads(result.output), - "SSH key added: aa:bb:cc:dd") + {'fingerprint': 'aa:bb:cc:dd', 'id': 1234, 'label': 'label'}) self.assert_called_with('SoftLayer_Security_Ssh_Key', 'createObject', args=({'notes': 'my key', 'key': mock_key, @@ -55,7 +55,7 @@ def test_add_by_file(self): self.assert_no_fail(result) self.assertEqual(json.loads(result.output), - "SSH key added: aa:bb:cc:dd") + {'fingerprint': 'aa:bb:cc:dd', 'id': 1234, 'label': 'label'}) service = self.client['Security_Ssh_Key'] mock_key = service.getObject()['key'] self.assert_called_with('SoftLayer_Security_Ssh_Key', 'createObject', diff --git a/tests/CLI/modules/subnet_tests.py b/tests/CLI/modules/subnet_tests.py index b1b7e8f2b..295964758 100644 --- a/tests/CLI/modules/subnet_tests.py +++ b/tests/CLI/modules/subnet_tests.py @@ -4,12 +4,17 @@ :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 import json +import mock +import SoftLayer class SubnetTests(testing.TestCase): + def test_detail(self): result = self.run_command(['subnet', 'detail', '1234']) @@ -31,7 +36,101 @@ def test_detail(self): 'private_ip': '10.0.1.2' } ], - 'hardware': 'none', + 'hardware': [], 'usable ips': 22 }, json.loads(result.output)) + + def test_list(self): + result = self.run_command(['subnet', 'list']) + self.assert_no_fail(result) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_subnet_ipv4(self, confirm_mock): + confirm_mock.return_value = True + + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems + + place_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + place_mock.return_value = SoftLayer_Product_Order.placeOrder + + result = self.run_command(['subnet', 'create', 'private', '8', '12346']) + self.assert_no_fail(result) + + output = [ + {'Item': 'Total monthly cost', 'cost': '0.00'} + ] + + self.assertEqual(output, json.loads(result.output)) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_subnet_ipv6(self, confirm_mock): + confirm_mock.return_value = True + + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems + + place_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + place_mock.return_value = SoftLayer_Product_Order.verifyOrder + + result = self.run_command(['subnet', 'create', '--v6', 'public', '64', '12346', '--test']) + self.assert_no_fail(result) + + output = [ + {'Item': 'this is a thing', 'cost': '2.00'}, + {'Item': 'Total monthly cost', 'cost': '2.00'} + ] + + self.assertEqual(output, json.loads(result.output)) + + def test_create_subnet_no_prices_found(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems + + verify_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + verify_mock.side_effect = SoftLayer.SoftLayerAPIError('SoftLayer_Exception', 'Price not found') + + result = self.run_command(['subnet', 'create', '--v6', 'public', '32', '12346', '--test']) + + self.assertRaises(SoftLayer.SoftLayerAPIError, verify_mock) + self.assertIn('Unable to order 32 public ipv6', result.exception.message, ) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_subnet_static(self, confirm_mock): + confirm_mock.return_value = True + + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems + + place_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + place_mock.return_value = SoftLayer_Product_Order.placeOrder + + result = self.run_command(['subnet', 'create', 'static', '2', '12346']) + self.assert_no_fail(result) + + output = [ + {'Item': 'Total monthly cost', 'cost': '0.00'} + ] + + self.assertEqual(output, json.loads(result.output)) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_subnet_static_ipv6(self, confirm_mock): + confirm_mock.return_value = True + + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems + + place_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + place_mock.return_value = SoftLayer_Product_Order.verifyOrder + + result = self.run_command(['subnet', 'create', '--v6', 'static', '64', '12346', '--test']) + self.assert_no_fail(result) + + output = [ + {'Item': 'this is a thing', 'cost': '2.00'}, + {'Item': 'Total monthly cost', 'cost': '2.00'} + ] + + self.assertEqual(output, json.loads(result.output)) diff --git a/tests/CLI/modules/ticket_tests.py b/tests/CLI/modules/ticket_tests.py index 817b3e71f..3f338cf1c 100644 --- a/tests/CLI/modules/ticket_tests.py +++ b/tests/CLI/modules/ticket_tests.py @@ -55,7 +55,6 @@ def test_create(self): self.assert_no_fail(result) args = ({'subjectId': 1000, - 'contents': 'ticket body', 'assignedUserId': 12345, 'title': 'Test'}, 'ticket body') @@ -70,7 +69,6 @@ def test_create_with_priority(self): self.assert_no_fail(result) args = ({'subjectId': 1000, - 'contents': 'ticket body', 'assignedUserId': 12345, 'title': 'Test', 'priority': 1}, 'ticket body') @@ -87,7 +85,6 @@ def test_create_and_attach(self): self.assert_no_fail(result) args = ({'subjectId': 1000, - 'contents': 'ticket body', 'assignedUserId': 12345, 'title': 'Test'}, 'ticket body') @@ -108,7 +105,6 @@ def test_create_no_body(self, edit_mock): self.assert_no_fail(result) args = ({'subjectId': 1000, - 'contents': 'ticket body', 'assignedUserId': 12345, 'title': 'Test'}, 'ticket body') @@ -277,12 +273,12 @@ def test_ticket_summary(self): 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}]}, + {'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']) diff --git a/tests/CLI/modules/user_tests.py b/tests/CLI/modules/user_tests.py index 0222a62b8..79554fc85 100644 --- a/tests/CLI/modules/user_tests.py +++ b/tests/CLI/modules/user_tests.py @@ -94,7 +94,7 @@ def test_print_hardware_access(self): 'fullyQualifiedDomainName': 'test.test.test', 'provisionDate': '2018-05-08T15:28:32-06:00', 'primaryBackendIpAddress': '175.125.126.118', - 'primaryIpAddress': '175.125.126.118'} + 'primaryIpAddress': '175.125.126.118'} ], 'dedicatedHosts': [ {'id': 1234, @@ -129,7 +129,7 @@ def test_edit_perms_on(self): 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) + self.assertEqual(result.exit_code, 1) def test_edit_perms_off(self): result = self.run_command(['user', 'edit-permissions', '11100', '--disable', '-p', 'TEST']) diff --git a/tests/CLI/modules/vs/__init__.py b/tests/CLI/modules/vs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/CLI/modules/vs/vs_capacity_tests.py b/tests/CLI/modules/vs/vs_capacity_tests.py new file mode 100644 index 000000000..2cee000a0 --- /dev/null +++ b/tests/CLI/modules/vs/vs_capacity_tests.py @@ -0,0 +1,92 @@ +""" + SoftLayer.tests.CLI.modules.vs.vs_capacity_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +import json +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_list_no_billing(self): + account_mock = self.set_mock('SoftLayer_Account', 'getReservedCapacityGroups') + account_mock.return_value = [ + { + 'id': 3103, + 'name': 'test-capacity', + 'createDate': '2018-09-24T16:33:09-06:00', + 'availableInstanceCount': 1, + 'instanceCount': 3, + 'occupiedInstanceCount': 1, + 'backendRouter': { + 'hostname': 'bcr02a.dal13', + }, + 'instances': [{'id': 3501}] + } + ] + result = self.run_command(['vs', 'capacity', 'list']) + self.assert_no_fail(result) + self.assertEqual(json.loads(result.output)[0]['Flavor'], 'Unknown Billing Item') + + 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 --git a/tests/CLI/modules/vs/vs_create_tests.py b/tests/CLI/modules/vs/vs_create_tests.py new file mode 100644 index 000000000..46d33057e --- /dev/null +++ b/tests/CLI/modules/vs/vs_create_tests.py @@ -0,0 +1,688 @@ +""" + SoftLayer.tests.CLI.modules.vs.vs_create_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +import mock +import sys +import tempfile + +from SoftLayer.fixtures import SoftLayer_Product_Package as SoftLayer_Product_Package +from SoftLayer import testing + + +class VirtCreateTests(testing.TestCase): + + @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', + '--hostnames=host', + '--os=UBUNTU_LATEST', + '--memory=1', + '--network=100', + '--billing=hourly', + '--datacenter=dal05', + '--tag=dev', + '--tag=green']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + args = ({'datacenter': {'name': 'dal05'}, + 'domain': 'example.com', + 'hourlyBillingFlag': True, + 'localDiskFlag': True, + 'maxMemory': 1024, + 'hostname': 'host', + 'startCpus': 2, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'networkComponents': [{'maxSpeed': '100'}], + 'supplementalCreateObjectOptions': {'bootMode': None}},) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', 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.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + args = ({ + 'startCpus': 2, + 'maxMemory': 1024, + 'hostname': 'host', + 'domain': 'example.com', + 'localDiskFlag': True, + 'hourlyBillingFlag': True, + 'supplementalCreateObjectOptions': {'bootMode': None}, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'datacenter': {'name': 'dal05'}, + 'primaryBackendNetworkComponent': { + 'networkVlan': { + 'id': 577940, + 'primarySubnet': {'id': 478700} + } + }, + 'primaryNetworkComponent': { + 'networkVlan': { + 'id': 1639255, + 'primarySubnet': {'id': 297614} + } + } + },) + + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) + + @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', + '--cpu=2', + '--domain=example.com', + '--hostname=host', + '--image=12345', + '--memory=1', + '--network=100', + '--billing=hourly', + '--datacenter=dal05']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_integer_image_guid(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'create', + '--cpu=2', + '--domain=example.com', + '--hostname=host', + '--image=aaaa1111bbbb2222', + '--memory=1', + '--network=100', + '--billing=hourly', + '--datacenter=dal05']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + args = ({ + 'startCpus': 2, + 'maxMemory': 1024, + 'hostname': 'host', + 'domain': 'example.com', + 'localDiskFlag': True, + 'hourlyBillingFlag': True, + 'supplementalCreateObjectOptions': {'bootMode': None}, + 'blockDeviceTemplateGroup': {'globalIdentifier': 'aaaa1111bbbb2222'}, + 'datacenter': {'name': 'dal05'}, + 'networkComponents': [{'maxSpeed': '100'}] + },) + + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_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', + '--billing=hourly', + '--datacenter=dal05', + '--flavor=B1_1X2X25']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + args = ({'datacenter': {'name': 'dal05'}, + 'domain': 'example.com', + 'hourlyBillingFlag': True, + 'hostname': 'host', + 'startCpus': None, + 'maxMemory': None, + 'localDiskFlag': None, + 'supplementalCreateObjectOptions': { + 'bootMode': None, + 'flavorKeyName': 'B1_1X2X25'}, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'networkComponents': [{'maxSpeed': '100'}]},) + + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', 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', + '--cpu=2', + '--domain=example.com', + '--hostname=host', + '--os=UBUNTU_LATEST', + '--memory=1', + '--network=100', + '--billing=hourly', + '--datacenter=dal05', + '--dedicated', + '--host-id=123']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + # Argument testing Example + order_call = self.calls('SoftLayer_Product_Order', 'placeOrder') + order_args = getattr(order_call[0], 'args')[0] + self.assertEqual(123, order_args['hostId']) + + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + template_args = ({ + 'startCpus': 2, + 'maxMemory': 1024, + 'hostname': 'host', + 'domain': 'example.com', + 'localDiskFlag': True, + 'hourlyBillingFlag': True, + 'supplementalCreateObjectOptions': { + 'bootMode': None + }, + 'dedicatedHost': { + 'id': 123 + }, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'datacenter': { + 'name': 'dal05' + }, + 'networkComponents': [ + { + 'maxSpeed': '100' + } + ] + },) + + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=template_args) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_like(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + 'hostname': 'vs-test-like', + 'domain': 'test.sftlyr.ws', + 'maxCpu': 2, + 'maxMemory': 1024, + 'datacenter': {'name': 'dal05'}, + 'networkComponents': [{'maxSpeed': 100}], + 'dedicatedAccountHostOnlyFlag': False, + 'privateNetworkOnlyFlag': False, + 'billingItem': {'orderItem': {'preset': {}}}, + 'operatingSystem': {'softwareLicense': { + 'softwareDescription': {'referenceCode': 'UBUNTU_LATEST'} + }}, + 'hourlyBillingFlag': False, + 'localDiskFlag': True, + 'userData': {} + } + + confirm_mock.return_value = True + result = self.run_command(['vs', 'create', + '--like=123', + '--san', + '--billing=hourly']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + args = ({'datacenter': {'name': 'dal05'}, + 'domain': 'test.sftlyr.ws', + 'hourlyBillingFlag': True, + 'hostname': 'vs-test-like', + 'startCpus': 2, + 'maxMemory': 1024, + 'localDiskFlag': False, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'networkComponents': [{'maxSpeed': 100}], + 'supplementalCreateObjectOptions': {'bootMode': None}},) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_like_tags(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + 'hostname': 'vs-test-like', + 'domain': 'test.sftlyr.ws', + 'maxCpu': 2, + 'maxMemory': 1024, + 'datacenter': {'name': 'dal05'}, + 'networkComponents': [{'maxSpeed': 100}], + 'dedicatedAccountHostOnlyFlag': False, + 'privateNetworkOnlyFlag': False, + 'billingItem': {'orderItem': {'preset': {}}}, + 'operatingSystem': {'softwareLicense': { + 'softwareDescription': {'referenceCode': 'UBUNTU_LATEST'} + }}, + 'hourlyBillingFlag': False, + 'localDiskFlag': True, + 'userData': {}, + 'tagReferences': [{'tag': {'name': 'production'}}], + } + + confirm_mock.return_value = True + result = self.run_command(['vs', 'create', + '--like=123', + '--san', + '--billing=hourly']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + _args = ('production',) + self.assert_called_with('SoftLayer_Virtual_Guest', 'setTags', identifier=1234567, args=_args) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_like_image(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + 'hostname': 'vs-test-like', + 'domain': 'test.sftlyr.ws', + 'maxCpu': 2, + 'maxMemory': 1024, + 'datacenter': {'name': 'dal05'}, + 'networkComponents': [{'maxSpeed': 100}], + 'dedicatedAccountHostOnlyFlag': False, + 'privateNetworkOnlyFlag': False, + 'billingItem': {'orderItem': {'preset': {}}}, + 'blockDeviceTemplateGroup': {'globalIdentifier': 'aaa1xxx1122233'}, + 'hourlyBillingFlag': False, + 'localDiskFlag': True, + 'userData': {}, + } + + confirm_mock.return_value = True + result = self.run_command(['vs', 'create', + '--like=123', + '--san', + '--billing=hourly']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + args = ({'datacenter': {'name': 'dal05'}, + 'domain': 'test.sftlyr.ws', + 'hourlyBillingFlag': True, + 'hostname': 'vs-test-like', + 'startCpus': 2, + 'maxMemory': 1024, + 'localDiskFlag': False, + 'blockDeviceTemplateGroup': {'globalIdentifier': 'aaa1xxx1122233'}, + 'networkComponents': [{'maxSpeed': 100}], + 'supplementalCreateObjectOptions': {'bootMode': None}},) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) # + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_like_flavor(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + 'hostname': 'vs-test-like', + 'domain': 'test.sftlyr.ws', + 'maxCpu': 2, + 'maxMemory': 1024, + 'datacenter': {'name': 'dal05'}, + 'networkComponents': [{'maxSpeed': 100}], + 'dedicatedAccountHostOnlyFlag': False, + 'privateNetworkOnlyFlag': False, + 'billingItem': {'orderItem': {'preset': {'keyName': 'B1_1X2X25'}}}, + 'operatingSystem': {'softwareLicense': { + 'softwareDescription': {'referenceCode': 'UBUNTU_LATEST'} + }}, + 'hourlyBillingFlag': True, + 'localDiskFlag': False, + 'userData': {} + } + + confirm_mock.return_value = True + result = self.run_command(['vs', 'create', '--like=123']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + args = ({'datacenter': {'name': 'dal05'}, + 'domain': 'test.sftlyr.ws', + 'hourlyBillingFlag': True, + 'hostname': 'vs-test-like', + 'startCpus': None, + 'maxMemory': None, + 'localDiskFlag': None, + 'supplementalCreateObjectOptions': { + 'bootMode': None, + 'flavorKeyName': 'B1_1X2X25'}, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'networkComponents': [{'maxSpeed': 100}]},) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_like_transient(self, confirm_mock): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + 'hostname': 'vs-test-like', + 'domain': 'test.sftlyr.ws', + 'datacenter': {'name': 'dal05'}, + 'networkComponents': [{'maxSpeed': 100}], + 'dedicatedAccountHostOnlyFlag': False, + 'privateNetworkOnlyFlag': False, + 'billingItem': {'orderItem': {'preset': {'keyName': 'B1_1X2X25'}}}, + 'operatingSystem': {'softwareLicense': { + 'softwareDescription': {'referenceCode': 'UBUNTU_LATEST'} + }}, + 'hourlyBillingFlag': True, + 'localDiskFlag': False, + 'transientGuestFlag': True, + 'userData': {} + } + + confirm_mock.return_value = True + result = self.run_command(['vs', 'create', '--like=123']) + + self.assert_no_fail(result) + self.assertIn('"guid": "1a2b3c-1701"', result.output) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + args = ({'datacenter': {'name': 'dal05'}, + 'domain': 'test.sftlyr.ws', + 'hourlyBillingFlag': True, + 'hostname': 'vs-test-like', + 'startCpus': None, + 'maxMemory': None, + 'localDiskFlag': None, + 'transientGuestFlag': True, + 'supplementalCreateObjectOptions': { + 'bootMode': None, + 'flavorKeyName': 'B1_1X2X25'}, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'networkComponents': [{'maxSpeed': 100}]},) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', 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', + 'B1_2X8X25', '--datacenter', 'TEST00']) + + self.assertEqual(2, result.exit_code) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_vs_transient(self, confirm_mock): + confirm_mock.return_value = True + + result = self.run_command(['vs', 'create', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', + 'B1_2X8X25', '--datacenter', 'TEST00', + '--transient', '--os', 'UBUNTU_LATEST']) + + self.assert_no_fail(result) + self.assertEqual(0, result.exit_code) + + def test_create_vs_bad_transient_monthly(self): + result = self.run_command(['vs', 'create', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', + 'B1_2X8X25', '--datacenter', 'TEST00', + '--transient', '--billing', 'monthly', + '--os', 'UBUNTU_LATEST']) + + self.assertEqual(2, result.exit_code) + + def test_create_vs_bad_transient_dedicated(self): + result = self.run_command(['vs', 'create', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', + 'B1_2X8X25', '--datacenter', 'TEST00', + '--transient', '--dedicated', + '--os', 'UBUNTU_LATEST']) + + self.assertEqual(2, result.exit_code) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_ipv6(self, confirm_mock): + amock = self.set_mock('SoftLayer_Product_Package', 'getItems') + amock.return_value = SoftLayer_Product_Package.getItems_1_IPV6_ADDRESS + result = self.run_command(['vs', 'create', '--test', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', 'B1_2X8X25', + '--datacenter', 'TEST00', '--os', 'UBUNTU_LATEST', '--ipv6']) + + self.assert_no_fail(result) + self.assertEqual(result.exit_code, 0) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + args = ({ + 'startCpus': None, + 'maxMemory': None, + 'hostname': 'TEST', + 'domain': 'TESTING', + 'localDiskFlag': None, + 'hourlyBillingFlag': True, + 'supplementalCreateObjectOptions': { + 'bootMode': None, + 'flavorKeyName': 'B1_2X8X25' + }, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'datacenter': { + 'name': 'TEST00' + } + }, + ) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) + self.assertEqual([], self.calls('SoftLayer_Virtual_Guest', 'setTags')) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_ipv6_no_test(self, confirm_mock): + confirm_mock.return_value = True + amock = self.set_mock('SoftLayer_Product_Package', 'getItems') + amock.return_value = SoftLayer_Product_Package.getItems_1_IPV6_ADDRESS + result = self.run_command(['vs', 'create', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', 'B1_2X8X25', + '--datacenter', 'TEST00', '--os', 'UBUNTU_LATEST', '--ipv6']) + + self.assert_no_fail(result) + self.assertEqual(result.exit_code, 0) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + self.assertEqual([], self.calls('SoftLayer_Virtual_Guest', 'setTags')) + + @mock.patch('SoftLayer.CLI.formatting.no_going_back') + def test_create_with_ipv6_no_prices(self, confirm_mock): + """Test makes sure create fails if ipv6 price cannot be found. + + Since its hard to test if the price ids gets added to placeOrder call, + this test juse makes sure that code block isn't being skipped + """ + result = self.run_command(['vs', 'create', '--test', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', 'B1_2X8X25', + '--datacenter', 'TEST00', '--os', 'UBUNTU_LATEST', + '--ipv6']) + self.assertEqual(result.exit_code, 1) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_vs_no_confirm(self, confirm_mock): + confirm_mock.return_value = False + + result = self.run_command(['vs', 'create', '--hostname', 'TEST', + '--domain', 'TESTING', '--flavor', 'B1_2X8X25', + '--datacenter', 'TEST00', '--os', 'UBUNTU_LATEST']) + + self.assertEqual(result.exit_code, 2) + + def test_create_vs_export(self): + if(sys.platform.startswith("win")): + self.skipTest("Test doesn't work in Windows") + with tempfile.NamedTemporaryFile() as config_file: + result = self.run_command(['vs', 'create', '--hostname', 'TEST', '--export', config_file.name, + '--domain', 'TESTING', '--flavor', 'B1_2X8X25', + '--datacenter', 'TEST00', '--os', 'UBUNTU_LATEST']) + self.assert_no_fail(result) + self.assertTrue('Successfully exported options to a template file.' + in result.output) + contents = config_file.read().decode("utf-8") + self.assertIn('hostname=TEST', contents) + self.assertIn('flavor=B1_2X8X25', contents) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_with_userdata(self, confirm_mock): + result = self.run_command(['vs', 'create', '--hostname', 'TEST', '--domain', 'TESTING', + '--flavor', 'B1_2X8X25', '--datacenter', 'TEST00', '--os', 'UBUNTU_LATEST', + '--userdata', 'This is my user data ok']) + self.assert_no_fail(result) + expected_guest = [ + { + 'domain': 'test.local', + 'hostname': 'test', + 'userData': [{'value': 'This is my user data ok'}] + } + ] + # Returns a list of API calls that hit SL_Product_Order::placeOrder + api_call = self.calls('SoftLayer_Product_Order', 'placeOrder') + # Doing this because the placeOrder args are huge and mostly not needed to test + self.assertEqual(api_call[0].args[0]['virtualGuests'], expected_guest) diff --git a/tests/CLI/modules/vs/vs_placement_tests.py b/tests/CLI/modules/vs/vs_placement_tests.py new file mode 100644 index 000000000..3b716a6cd --- /dev/null +++ b/tests/CLI/modules/vs/vs_placement_tests.py @@ -0,0 +1,109 @@ +""" + SoftLayer.tests.CLI.modules.vs_placement_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +import mock + +from SoftLayer import testing + + +class VSPlacementTests(testing.TestCase): + + def test_create_options(self): + result = self.run_command(['vs', 'placementgroup', 'create-options']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getAvailableRouters') + self.assert_called_with('SoftLayer_Virtual_PlacementGroup_Rule', 'getAllObjects') + self.assertEqual([], self.calls('SoftLayer_Virtual_PlacementGroup', 'createObject')) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_group(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'placementgroup', 'create', '--name=test', '--backend_router=1', '--rule=2']) + create_args = { + 'name': 'test', + 'backendRouterId': 1, + 'ruleId': 2 + } + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'createObject', args=(create_args,)) + self.assertEqual([], self.calls('SoftLayer_Virtual_PlacementGroup', 'getAvailableRouters')) + + def test_list_groups(self): + result = self.run_command(['vs', 'placementgroup', 'list']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getPlacementGroups') + + def test_detail_group_id(self): + result = self.run_command(['vs', 'placementgroup', 'detail', '12345']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject', identifier=12345) + + def test_detail_group_name(self): + result = self.run_command(['vs', 'placementgroup', 'detail', 'test']) + self.assert_no_fail(result) + group_filter = { + 'placementGroups': { + 'name': {'operation': 'test'} + } + } + self.assert_called_with('SoftLayer_Account', 'getPlacementGroups', filter=group_filter) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject', identifier=12345) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_delete_group_id(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'placementgroup', 'delete', '12345']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'deleteObject', identifier=12345) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_delete_group_id_cancel(self, confirm_mock): + confirm_mock.return_value = False + result = self.run_command(['vs', 'placementgroup', 'delete', '12345']) + self.assertEqual(result.exit_code, 2) + self.assertEqual([], self.calls('SoftLayer_Virtual_PlacementGroup', 'deleteObject')) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_delete_group_name(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'placementgroup', 'delete', 'test']) + group_filter = { + 'placementGroups': { + 'name': {'operation': 'test'} + } + } + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getPlacementGroups', filter=group_filter) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'deleteObject', identifier=12345) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_delete_group_purge(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'placementgroup', 'delete', '1234', '--purge']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject') + self.assert_called_with('SoftLayer_Virtual_Guest', 'deleteObject', identifier=69131875) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_delete_group_purge_cancel(self, confirm_mock): + confirm_mock.return_value = False + result = self.run_command(['vs', 'placementgroup', 'delete', '1234', '--purge']) + self.assertEqual(result.exit_code, 2) + self.assertEqual([], self.calls('SoftLayer_Virtual_Guest', 'deleteObject')) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_delete_group_purge_nothing(self, confirm_mock): + group_mock = self.set_mock('SoftLayer_Virtual_PlacementGroup', 'getObject') + group_mock.return_value = { + "id": 1234, + "name": "test-group", + "guests": [], + } + confirm_mock.return_value = True + result = self.run_command(['vs', 'placementgroup', 'delete', '1234', '--purge']) + self.assertEqual(result.exit_code, 2) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject') + self.assertEqual([], self.calls('SoftLayer_Virtual_Guest', 'deleteObject')) diff --git a/tests/CLI/modules/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py similarity index 54% rename from tests/CLI/modules/vs_tests.py rename to tests/CLI/modules/vs/vs_tests.py index c976a952e..203230913 100644 --- a/tests/CLI/modules/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -5,10 +5,12 @@ :license: MIT, see LICENSE for more details. """ import json +import sys import mock from SoftLayer.CLI import exceptions +from SoftLayer.fixtures import SoftLayer_Virtual_Guest as SoftLayer_Virtual_Guest from SoftLayer import SoftLayerAPIError from SoftLayer import testing @@ -168,78 +170,20 @@ def mock_lookup_func(dic, key, *keys): 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}) + output = json.loads(result.output) + self.assertEqual(output['owner'], None) def test_detail_vs(self): - result = self.run_command(['vs', 'detail', '100', - '--passwords', '--price']) + 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': 6.54, - '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': 'chechu'}) + output = json.loads(result.output) + self.assertEqual(output['notes'], 'notes') + self.assertEqual(output['price_rate'], 6.54) + self.assertEqual(output['users'][0]['username'], 'user') + self.assertEqual(output['vlans'][0]['number'], 23) + self.assertEqual(output['owner'], 'chechu') + self.assertEqual(output['Bandwidth'][0]['Allotment'], '250') def test_detail_vs_empty_tag(self): mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') @@ -278,12 +222,53 @@ def test_detail_vs_no_dedicated_host_hostname(self): self.assertEqual(json.loads(result.output)['dedicated_host_id'], 37401) self.assertIsNone(json.loads(result.output)['dedicated_host']) + def test_detail_vs_security_group(self): + vg_return = SoftLayer_Virtual_Guest.getObject + sec_group = [ + { + 'id': 35386715, + 'name': 'eth', + 'port': 0, + 'speed': 100, + 'status': 'ACTIVE', + 'primaryIpAddress': '10.175.106.149', + 'securityGroupBindings': [ + { + 'id': 1620971, + 'networkComponentId': 35386715, + 'securityGroupId': 128321, + 'securityGroup': { + 'id': 128321, + 'name': 'allow_all' + } + } + ] + } + ] + + vg_return['networkComponents'] = sec_group + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = vg_return + result = self.run_command(['vs', 'detail', '100']) + self.assert_no_fail(result) + output = json.loads(result.output) + self.assertEqual(output['security_groups'][0]['id'], 128321) + self.assertEqual(output['security_groups'][0]['name'], 'allow_all') + self.assertEqual(output['security_groups'][0]['interface'], 'PRIVATE') + + def test_detail_vs_ptr_error(self): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getReverseDomainRecords') + mock.side_effect = SoftLayerAPIError("SoftLayer_Exception", "Not Found") + result = self.run_command(['vs', 'detail', '100']) + self.assert_no_fail(result) + output = json.loads(result.output) + self.assertEqual(output.get('ptr', None), None) + def test_create_options(self): result = self.run_command(['vs', 'create-options']) self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - {'cpus (dedicated host)': [4, 56], + self.assertEqual({'cpus (dedicated host)': [4, 56], 'cpus (dedicated)': [1], 'cpus (standard)': [1, 2, 3, 4], 'datacenter': ['ams01', 'dal05'], @@ -293,6 +278,7 @@ def test_create_options(self): 'flavors (compute)': ['C1_1X2X25'], 'flavors (memory)': ['M1_1X2X100'], 'flavors (GPU)': ['AC1_1X2X100', 'ACL1_1X2X100'], + 'flavors (transient)': ['B1_1X2X25_TRANSIENT'], 'local disk(0)': ['25', '100'], 'memory': [1024, 2048, 3072, 4096], 'memory (dedicated host)': [8192, 65536], @@ -300,387 +286,8 @@ def test_create_options(self): 'nic (dedicated host)': ['1000'], 'os (CENTOS)': 'CENTOS_6_64', 'os (DEBIAN)': 'DEBIAN_7_64', - 'os (UBUNTU)': 'UBUNTU_12_64'}) - - @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', - '--hostname=host', - '--os=UBUNTU_LATEST', - '--memory=1', - '--network=100', - '--billing=hourly', - '--datacenter=dal05', - '--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'}) - - args = ({'datacenter': {'name': 'dal05'}, - 'domain': 'example.com', - 'hourlyBillingFlag': True, - 'localDiskFlag': True, - 'maxMemory': 1024, - 'hostname': 'host', - 'startCpus': 2, - 'operatingSystemReferenceCode': 'UBUNTU_LATEST', - 'networkComponents': [{'maxSpeed': '100'}], - 'supplementalCreateObjectOptions': {'bootMode': None}},) - self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject', - 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', - '--cpu=2', - '--domain=example.com', - '--hostname=host', - '--image=12345', - '--memory=1', - '--network=100', - '--billing=hourly', - '--datacenter=dal05']) - - self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - {'guid': '1a2b3c-1701', - 'id': 100, - 'created': '2013-08-01 15:23:45'}) - - args = ({ - 'datacenter': {'name': 'dal05'}, - 'domain': 'example.com', - 'hourlyBillingFlag': True, - 'localDiskFlag': True, - 'maxMemory': 1024, - 'hostname': 'host', - 'startCpus': 2, - 'blockDeviceTemplateGroup': { - 'globalIdentifier': '0B5DEAF4-643D-46CA-A695-CECBE8832C9D', - }, - 'networkComponents': [{'maxSpeed': '100'}], - 'supplementalCreateObjectOptions': {'bootMode': None} - },) - self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject', - args=args) - - @mock.patch('SoftLayer.CLI.formatting.confirm') - def test_create_with_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', - '--billing=hourly', - '--datacenter=dal05', - '--flavor=B1_1X2X25']) - - self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - {'guid': '1a2b3c-1701', - 'id': 100, - 'created': '2013-08-01 15:23:45'}) - - args = ({'datacenter': {'name': 'dal05'}, - 'domain': 'example.com', - 'hourlyBillingFlag': True, - 'hostname': 'host', - 'startCpus': None, - 'maxMemory': None, - 'localDiskFlag': None, - 'supplementalCreateObjectOptions': { - 'bootMode': None, - 'flavorKeyName': 'B1_1X2X25'}, - 'operatingSystemReferenceCode': 'UBUNTU_LATEST', - 'networkComponents': [{'maxSpeed': '100'}]},) - self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject', - 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', - '--cpu=2', - '--domain=example.com', - '--hostname=host', - '--os=UBUNTU_LATEST', - '--memory=1', - '--network=100', - '--billing=hourly', - '--datacenter=dal05', - '--dedicated', - '--host-id=123']) - - self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - {'guid': '1a2b3c-1701', - 'id': 100, - 'created': '2013-08-01 15:23:45'}) - - args = ({'datacenter': {'name': 'dal05'}, - 'domain': 'example.com', - 'hourlyBillingFlag': True, - 'localDiskFlag': True, - 'maxMemory': 1024, - 'hostname': 'host', - 'startCpus': 2, - 'operatingSystemReferenceCode': 'UBUNTU_LATEST', - 'networkComponents': [{'maxSpeed': '100'}], - 'dedicatedHost': {'id': 123}, - 'supplementalCreateObjectOptions': {'bootMode': None}},) - self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject', - args=args) - - @mock.patch('SoftLayer.CLI.formatting.confirm') - def test_create_like(self, confirm_mock): - mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') - mock.return_value = { - 'hostname': 'vs-test-like', - 'domain': 'test.sftlyr.ws', - 'maxCpu': 2, - 'maxMemory': 1024, - 'datacenter': {'name': 'dal05'}, - 'networkComponents': [{'maxSpeed': 100}], - 'dedicatedAccountHostOnlyFlag': False, - 'privateNetworkOnlyFlag': False, - 'billingItem': {'orderItem': {'preset': {}}}, - 'operatingSystem': {'softwareLicense': { - 'softwareDescription': {'referenceCode': 'UBUNTU_LATEST'} - }}, - 'hourlyBillingFlag': False, - 'localDiskFlag': True, - 'userData': {} - } - - confirm_mock.return_value = True - result = self.run_command(['vs', 'create', - '--like=123', - '--san', - '--billing=hourly']) - - self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - {'guid': '1a2b3c-1701', - 'id': 100, - 'created': '2013-08-01 15:23:45'}) - - args = ({'datacenter': {'name': 'dal05'}, - 'domain': 'test.sftlyr.ws', - 'hourlyBillingFlag': True, - 'hostname': 'vs-test-like', - 'startCpus': 2, - 'maxMemory': 1024, - 'localDiskFlag': False, - 'operatingSystemReferenceCode': 'UBUNTU_LATEST', - 'networkComponents': [{'maxSpeed': 100}], - 'supplementalCreateObjectOptions': {'bootMode': None}},) - self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject', - args=args) - - @mock.patch('SoftLayer.CLI.formatting.confirm') - def test_create_like_flavor(self, confirm_mock): - mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') - mock.return_value = { - 'hostname': 'vs-test-like', - 'domain': 'test.sftlyr.ws', - 'maxCpu': 2, - 'maxMemory': 1024, - 'datacenter': {'name': 'dal05'}, - 'networkComponents': [{'maxSpeed': 100}], - 'dedicatedAccountHostOnlyFlag': False, - 'privateNetworkOnlyFlag': False, - 'billingItem': {'orderItem': {'preset': {'keyName': 'B1_1X2X25'}}}, - 'operatingSystem': {'softwareLicense': { - 'softwareDescription': {'referenceCode': 'UBUNTU_LATEST'} - }}, - 'hourlyBillingFlag': True, - 'localDiskFlag': False, - 'userData': {} - } - - confirm_mock.return_value = True - result = self.run_command(['vs', 'create', '--like=123']) - - self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - {'guid': '1a2b3c-1701', - 'id': 100, - 'created': '2013-08-01 15:23:45'}) - - args = ({'datacenter': {'name': 'dal05'}, - 'domain': 'test.sftlyr.ws', - 'hourlyBillingFlag': True, - 'hostname': 'vs-test-like', - 'startCpus': None, - 'maxMemory': None, - 'localDiskFlag': None, - 'supplementalCreateObjectOptions': { - 'bootMode': None, - 'flavorKeyName': 'B1_1X2X25'}, - 'operatingSystemReferenceCode': 'UBUNTU_LATEST', - 'networkComponents': [{'maxSpeed': 100}]},) - self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject', - 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, -1) - - 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) + 'os (UBUNTU)': 'UBUNTU_12_64'}, + json.loads(result.output)) @mock.patch('SoftLayer.CLI.formatting.confirm') def test_dns_sync_both(self, confirm_mock): @@ -901,6 +508,26 @@ def test_upgrade(self, confirm_mock): 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}]) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_upgrade_with_cpu_memory_and_flavor(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'upgrade', '100', '--cpu=4', + '--memory=1024', '--flavor=M1_64X512X100']) + self.assertEqual(result.exit_code, 1) + self.assertIsInstance(result.exception, ValueError) + def test_edit(self): result = self.run_command(['vs', 'edit', '--domain=example.com', @@ -1014,3 +641,90 @@ def test_cancel_no_confirm(self, confirm_mock): result = self.run_command(['vs', 'cancel', '100']) self.assertEqual(result.exit_code, 2) + + def test_vs_capture(self): + + result = self.run_command(['vs', 'capture', '100', '--name', 'TestName']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_Guest', 'createArchiveTransaction', identifier=100) + + @mock.patch('SoftLayer.CLI.formatting.no_going_back') + def test_usage_no_confirm(self, confirm_mock): + confirm_mock.return_value = False + + result = self.run_command(['vs', 'usage', '100']) + self.assertEqual(result.exit_code, 2) + + def test_usage_vs(self): + result = self.run_command( + ['vs', 'usage', '100']) + self.assertEqual(result.exit_code, 2) + + def test_usage_vs_cpu(self): + result = self.run_command( + ['vs', 'usage', '100', '--start_date=2019-3-4', '--end_date=2019-4-2', '--valid_type=CPU0', + '--summary_period=300']) + + self.assert_no_fail(result) + + def test_usage_vs_memory(self): + result = self.run_command( + ['vs', 'usage', '100', '--start_date=2019-3-4', '--end_date=2019-4-2', '--valid_type=MEMORY_USAGE', + '--summary_period=300']) + + self.assert_no_fail(result) + + def test_usage_metric_data_empty(self): + usage_vs = self.set_mock('SoftLayer_Metric_Tracking_Object', 'getSummaryData') + test_usage = [] + usage_vs.return_value = test_usage + result = self.run_command( + ['vs', 'usage', '100', '--start_date=2019-3-4', '--end_date=2019-4-2', '--valid_type=CPU0', + '--summary_period=300']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + def test_bandwidth_vs(self): + if sys.version_info < (3, 6): + self.skipTest("Test requires python 3.6+") + + result = self.run_command(['vs', 'bandwidth', '100', '--start_date=2019-01-01', '--end_date=2019-02-01']) + self.assert_no_fail(result) + + date = '2019-05-20 23:00' + # number of characters from the end of output to break so json can parse properly + pivot = 157 + # only pyhon 3.7 supports the timezone format slapi uses + if sys.version_info < (3, 7): + date = '2019-05-20T23:00:00-06:00' + pivot = 166 + # Since this is 2 tables, it gets returned as invalid json like "[{}][{}]"" instead of "[[{}],[{}]]" + # so we just do some hacky string substitution to pull out the respective arrays that can be jsonifyied + + output_summary = json.loads(result.output[0:-pivot]) + output_list = json.loads(result.output[-pivot:]) + + self.assertEqual(output_summary[0]['Average MBps'], 0.3841) + self.assertEqual(output_summary[1]['Max Date'], date) + self.assertEqual(output_summary[2]['Max GB'], 0.1172) + self.assertEqual(output_summary[3]['Sum GB'], 0.0009) + + self.assertEqual(output_list[0]['Date'], date) + self.assertEqual(output_list[0]['Pub In'], 1.3503) + + def test_bandwidth_vs_quite(self): + result = self.run_command(['vs', 'bandwidth', '100', '--start_date=2019-01-01', '--end_date=2019-02-01', '-q']) + self.assert_no_fail(result) + + date = '2019-05-20 23:00' + + # only pyhon 3.7 supports the timezone format slapi uses + if sys.version_info < (3, 7): + date = '2019-05-20T23:00:00-06:00' + + output_summary = json.loads(result.output) + + self.assertEqual(output_summary[0]['Average MBps'], 0.3841) + self.assertEqual(output_summary[1]['Max Date'], date) + self.assertEqual(output_summary[2]['Max GB'], 0.1172) + self.assertEqual(output_summary[3]['Sum GB'], 0.0009) diff --git a/tests/api_tests.py b/tests/api_tests.py index 458153de4..4f1a31e66 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -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', diff --git a/tests/managers/account_tests.py b/tests/managers/account_tests.py new file mode 100644 index 000000000..7efc42acd --- /dev/null +++ b/tests/managers/account_tests.py @@ -0,0 +1,54 @@ +""" + SoftLayer.tests.managers.account_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +""" + +from SoftLayer.managers.account import AccountManager as AccountManager +from SoftLayer import testing + + +class AccountManagerTests(testing.TestCase): + + def set_up(self): + self.manager = AccountManager(self.client) + self.SLNOE = 'SoftLayer_Notification_Occurrence_Event' + + def test_get_summary(self): + self.manager.get_summary() + self.assert_called_with('SoftLayer_Account', 'getObject') + + def test_get_upcoming_events(self): + self.manager.get_upcoming_events() + self.assert_called_with(self.SLNOE, 'getAllObjects') + + def test_ack_event(self): + self.manager.ack_event(12345) + self.assert_called_with(self.SLNOE, 'acknowledgeNotification', identifier=12345) + + def test_get_event(self): + self.manager.get_event(12345) + self.assert_called_with(self.SLNOE, 'getObject', identifier=12345) + + def test_get_invoices(self): + self.manager.get_invoices() + self.assert_called_with('SoftLayer_Account', 'getInvoices') + + def test_get_invoices_closed(self): + self.manager.get_invoices(closed=True) + _filter = { + 'invoices': { + 'createDate': { + 'operation': 'orderBy', + 'options': [{ + 'name': 'sort', + 'value': ['DESC'] + }] + } + } + } + self.assert_called_with('SoftLayer_Account', 'getInvoices', filter=_filter) + + def test_get_billing_items(self): + self.manager.get_billing_items(12345) + self.assert_called_with('SoftLayer_Billing_Invoice', 'getInvoiceTopLevelItems') diff --git a/tests/managers/cdn_tests.py b/tests/managers/cdn_tests.py index 6f4387760..41a675c6d 100644 --- a/tests/managers/cdn_tests.py +++ b/tests/managers/cdn_tests.py @@ -4,11 +4,10 @@ :license: MIT, see LICENSE for more details. """ -import math -from SoftLayer import fixtures from SoftLayer.managers import cdn from SoftLayer import testing +from SoftLayer import utils class CDNTests(testing.TestCase): @@ -17,118 +16,96 @@ def set_up(self): self.cdn_client = cdn.CDNManager(self.client) def test_list_accounts(self): - accounts = self.cdn_client.list_accounts() - self.assertEqual(accounts, fixtures.SoftLayer_Account.getCdnAccounts) + self.cdn_client.list_cdn() + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping', + 'listDomainMappings') - def test_get_account(self): - account = self.cdn_client.get_account(12345) - self.assertEqual( - account, - fixtures.SoftLayer_Network_ContentDelivery_Account.getObject) + def test_detail_cdn(self): + self.cdn_client.get_cdn("12345") + + args = ("12345",) + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping', + 'listDomainMappingByUniqueId', + args=args) + + def test_detail_usage_metric(self): + self.cdn_client.get_usage_metrics(12345, history=30, frequency="aggregate") + + _start = utils.days_to_datetime(30) + _end = utils.days_to_datetime(0) + + _start_date = utils.timestamp(_start) + _end_date = utils.timestamp(_end) + + args = (12345, + _start_date, + _end_date, + "aggregate") + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Metrics', + 'getMappingUsageMetrics', + args=args) def test_get_origins(self): - origins = self.cdn_client.get_origins(12345) - self.assertEqual( - origins, - fixtures.SoftLayer_Network_ContentDelivery_Account. - getOriginPullMappingInformation) + self.cdn_client.get_origins("12345") + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', + 'listOriginPath') def test_add_origin(self): - self.cdn_client.add_origin(12345, - 'http', - 'http://localhost/', - 'self.local', - False) + self.cdn_client.add_origin("12345", "10.10.10.1", "/example/videos", origin_type="server", + header="test.example.com", port=80, protocol='http', optimize_for="web", + cache_query="include all") args = ({ - 'mediaType': 'http', - 'originUrl': 'http://localhost/', - 'cname': 'self.local', - 'isSecureContent': False - },) - self.assert_called_with('SoftLayer_Network_ContentDelivery_Account', - 'createOriginPullMapping', - args=args, - identifier=12345) + 'uniqueId': "12345", + 'origin': '10.10.10.1', + 'path': '/example/videos', + 'originType': 'HOST_SERVER', + 'header': 'test.example.com', + 'httpPort': 80, + 'protocol': 'HTTP', + 'performanceConfiguration': 'General web delivery', + 'cacheKeyQueryRule': "include all" + },) + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', + 'createOriginPath', + args=args) + + def test_add_origin_with_bucket_and_file_extension(self): + self.cdn_client.add_origin("12345", "10.10.10.1", "/example/videos", origin_type="storage", + bucket_name="test-bucket", file_extensions="jpg", header="test.example.com", port=80, + protocol='http', optimize_for="web", cache_query="include all") + + args = ({ + 'uniqueId': "12345", + 'origin': '10.10.10.1', + 'path': '/example/videos', + 'originType': 'OBJECT_STORAGE', + 'header': 'test.example.com', + 'httpPort': 80, + 'protocol': 'HTTP', + 'bucketName': 'test-bucket', + 'fileExtension': 'jpg', + 'performanceConfiguration': 'General web delivery', + 'cacheKeyQueryRule': "include all" + },) + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', + 'createOriginPath', + args=args) def test_remove_origin(self): - self.cdn_client.remove_origin(12345, 12345) - self.assert_called_with('SoftLayer_Network_ContentDelivery_Account', - 'deleteOriginPullRule', - args=(12345,), - identifier=12345) - - def test_load_content(self): - urls = ['http://a/img/0x001.png', - 'http://b/img/0x002.png', - 'http://c/img/0x004.png', - 'http://d/img/0x008.png', - 'http://e/img/0x010.png', - 'http://e/img/0x020.png'] - - self.cdn_client.load_content(12345, urls) - calls = self.calls('SoftLayer_Network_ContentDelivery_Account', - 'loadContent') - self.assertEqual(len(calls), - math.ceil(len(urls) / float(cdn.MAX_URLS_PER_LOAD))) - - def test_load_content_single(self): - url = 'http://geocities.com/Area51/Meteor/12345/under_construction.gif' - self.cdn_client.load_content(12345, url) - - self.assert_called_with('SoftLayer_Network_ContentDelivery_Account', - 'loadContent', - args=([url],), - identifier=12345) - - def test_load_content_failure(self): - urls = ['http://z/img/0x004.png', - 'http://y/img/0x002.png', - 'http://x/img/0x001.png'] - - service = self.client['SoftLayer_Network_ContentDelivery_Account'] - service.loadContent.return_value = False - - self.cdn_client.load_content(12345, urls) - calls = self.calls('SoftLayer_Network_ContentDelivery_Account', - 'loadContent') - self.assertEqual(len(calls), - math.ceil(len(urls) / float(cdn.MAX_URLS_PER_LOAD))) + self.cdn_client.remove_origin("12345", "/example1") + + args = ("12345", + "/example1") + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Mapping_Path', + 'deleteOriginPath', + args=args) def test_purge_content(self): - urls = ['http://z/img/0x020.png', - 'http://y/img/0x010.png', - 'http://x/img/0x008.png', - 'http://w/img/0x004.png', - 'http://v/img/0x002.png', - 'http://u/img/0x001.png'] - - self.cdn_client.purge_content(12345, urls) - calls = self.calls('SoftLayer_Network_ContentDelivery_Account', - 'purgeCache') - self.assertEqual(len(calls), - math.ceil(len(urls) / float(cdn.MAX_URLS_PER_PURGE))) - - def test_purge_content_failure(self): - urls = ['http://z/img/0x004.png', - 'http://y/img/0x002.png', - 'http://x/img/0x001.png'] - - mock = self.set_mock('SoftLayer_Network_ContentDelivery_Account', - 'purgeCache') - mock.return_value = False - - self.cdn_client.purge_content(12345, urls) - calls = self.calls('SoftLayer_Network_ContentDelivery_Account', - 'purgeCache') - self.assertEqual(len(calls), - math.ceil(len(urls) / float(cdn.MAX_URLS_PER_PURGE))) - - def test_purge_content_single(self): - url = 'http://geocities.com/Area51/Meteor/12345/under_construction.gif' - - self.cdn_client.purge_content(12345, url) - self.assert_called_with('SoftLayer_Network_ContentDelivery_Account', - 'purgeCache', - args=([url],), - identifier=12345) + self.cdn_client.purge_content("12345", "/example1") + + args = ("12345", + "/example1") + self.assert_called_with('SoftLayer_Network_CdnMarketplace_Configuration_Cache_Purge', + 'createPurge', + args=args) diff --git a/tests/managers/dedicated_host_tests.py b/tests/managers/dedicated_host_tests.py index d6ced1305..de376c007 100644 --- a/tests/managers/dedicated_host_tests.py +++ b/tests/managers/dedicated_host_tests.py @@ -78,7 +78,14 @@ def test_get_host(self): domain, uuid ], - guestCount + guestCount, + tagReferences[ + id, + tag[ + name, + id + ] + ] ''') self.dedicated_host.host.getObject.assert_called_once_with(id=12345, mask=mask) @@ -90,7 +97,7 @@ def test_place_order(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': u'test.com', @@ -103,7 +110,7 @@ def test_place_order(self): 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -116,13 +123,64 @@ def test_place_order(self): hourly = True flavor = '56_CORES_X_242_RAM_X_1_4_TB' - self.dedicated_host.place_order(hostname=hostname, + self.dedicated_host.place_order(hostnames=[hostname], + domain=domain, + location=location, + flavor=flavor, + hourly=hourly) + + create_dict.assert_called_once_with(hostnames=[hostname], + router=None, + domain=domain, + datacenter=location, + flavor=flavor, + hourly=True) + + self.assert_called_with('SoftLayer_Product_Order', + '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(hostnames=[hostname], domain=domain, location=location, flavor=flavor, hourly=hourly) - create_dict.assert_called_once_with(hostname=hostname, + create_dict.assert_called_once_with(hostnames=[hostname], router=None, domain=domain, datacenter=location, @@ -141,7 +199,7 @@ def test_verify_order(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -154,7 +212,7 @@ def test_verify_order(self): 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -167,13 +225,13 @@ def test_verify_order(self): hourly = True flavor = '56_CORES_X_242_RAM_X_1_4_TB' - self.dedicated_host.verify_order(hostname=hostname, + self.dedicated_host.verify_order(hostnames=[hostname], domain=domain, location=location, flavor=flavor, hourly=hourly) - create_dict.assert_called_once_with(hostname=hostname, + create_dict.assert_called_once_with(hostnames=[hostname], router=None, domain=domain, datacenter=location, @@ -197,7 +255,7 @@ def test_generate_create_dict_without_router(self): hourly = True flavor = '56_CORES_X_242_RAM_X_1_4_TB' - results = self.dedicated_host._generate_create_dict(hostname=hostname, + results = self.dedicated_host._generate_create_dict(hostnames=[hostname], domain=domain, datacenter=location, flavor=flavor, @@ -208,7 +266,7 @@ def test_generate_create_dict_without_router(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -221,7 +279,7 @@ def test_generate_create_dict_without_router(self): 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -233,17 +291,17 @@ def test_generate_create_dict_with_router(self): 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 flavor = '56_CORES_X_242_RAM_X_1_4_TB' results = self.dedicated_host._generate_create_dict( - hostname=hostname, + hostnames=[hostname], router=router, domain=domain, datacenter=location, @@ -255,7 +313,7 @@ def test_generate_create_dict_with_router(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -269,7 +327,7 @@ def test_generate_create_dict_with_router(self): 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -286,7 +344,8 @@ def test_get_package(self): capacity, keyName, itemCategory[categoryCode], - bundleItems[capacity, categories[categoryCode]] + bundleItems[capacity,keyName,categories[categoryCode],hardwareGenericComponentModel[id, + hardwareComponentType[keyName]]] ], regions[location[location[priceGroups]]] ''' @@ -369,7 +428,7 @@ def test_get_create_options(self): 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 +447,29 @@ def test_get_item(self): 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 +488,7 @@ def test_get_backend_router(self): location = [ { 'isAvailable': 1, - 'locationId': 138124, + 'locationId': 12345, 'packageId': 813 } ] @@ -474,7 +535,7 @@ def test_get_backend_router_no_routers_found(self): 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 +547,64 @@ def test_get_default_router_no_router_found(self): 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 +619,7 @@ def _get_package(self): "bundleItems": [ { "capacity": "1200", + "keyName": "1_4_TB_LOCAL_STORAGE_DEDICATED_HOST_CAPACITY", "categories": [ { "categoryCode": "dedicated_host_disk" @@ -525,6 +628,7 @@ def _get_package(self): }, { "capacity": "242", + "keyName": "242_GB_RAM", "categories": [ { "categoryCode": "dedicated_host_ram" @@ -534,14 +638,14 @@ def _get_package(self): ], "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 +656,12 @@ def _get_package(self): "location": { "locationPackageDetails": [ { - "locationId": 265592, + "locationId": 12345, "packageId": 813 } ], "location": { - "id": 265592, + "id": 12345, "name": "ams01", "longName": "Amsterdam 1" } @@ -571,12 +675,12 @@ def _get_package(self): "locationPackageDetails": [ { "isAvailable": 1, - "locationId": 138124, + "locationId": 12345, "packageId": 813 } ], "location": { - "id": 138124, + "id": 12345, "name": "dal05", "longName": "Dallas 5" } @@ -591,3 +695,34 @@ def _get_package(self): } 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 --git a/tests/managers/dns_tests.py b/tests/managers/dns_tests.py index 8cd83a3a2..6b32af918 100644 --- a/tests/managers/dns_tests.py +++ b/tests/managers/dns_tests.py @@ -91,6 +91,73 @@ def test_create_record(self): },)) 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 --git a/tests/managers/event_log_tests.py b/tests/managers/event_log_tests.py new file mode 100644 index 000000000..e5c220835 --- /dev/null +++ b/tests/managers/event_log_tests.py @@ -0,0 +1,253 @@ +""" + SoftLayer.tests.managers.event_log_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +import SoftLayer +from SoftLayer import fixtures +from SoftLayer import testing + + +class EventLogTests(testing.TestCase): + + def set_up(self): + self.event_log = SoftLayer.EventLogManager(self.client) + + def test_get_event_logs(self): + # Cast to list to force generator to get all objects + result = list(self.event_log.get_event_logs()) + + expected = fixtures.SoftLayer_Event_Log.getAllObjects + self.assertEqual(expected, result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects') + + def test_get_event_logs_no_iteration(self): + # Cast to list to force generator to get all objects + result = self.event_log.get_event_logs(iterator=False) + + expected = fixtures.SoftLayer_Event_Log.getAllObjects + self.assertEqual(expected, result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects') + + def test_get_event_log_types(self): + result = self.event_log.get_event_log_types() + + expected = fixtures.SoftLayer_Event_Log.getAllEventObjectNames + self.assertEqual(expected, result) + self.assert_called_with('SoftLayer_Event_Log', 'getAllEventObjectNames') + + def test_build_filter_no_args(self): + result = self.event_log.build_filter(None, None, None, None, None, None) + + self.assertEqual(result, {}) + + def test_build_filter_min_date(self): + expected = { + 'eventCreateDate': { + 'operation': 'greaterThanDate', + 'options': [ + { + 'name': 'date', + 'value': [ + '2017-10-30T00:00:00.000000+00:00' + ] + } + ] + } + } + + result = self.event_log.build_filter('10/30/2017', None, None, None, None, None) + + self.assertEqual(expected, result) + + def test_build_filter_max_date(self): + expected = { + 'eventCreateDate': { + 'operation': 'lessThanDate', + 'options': [ + { + 'name': 'date', + 'value': [ + '2017-10-31T00:00:00.000000+00:00' + ] + } + ] + } + } + + result = self.event_log.build_filter(None, '10/31/2017', None, None, None, None) + + self.assertEqual(expected, result) + + def test_build_filter_min_max_date(self): + expected = { + 'eventCreateDate': { + 'operation': 'betweenDate', + 'options': [ + { + 'name': 'startDate', + 'value': [ + '2017-10-30T00:00:00.000000+00:00' + ] + }, + { + 'name': 'endDate', + 'value': [ + '2017-10-31T00:00:00.000000+00:00' + ] + } + ] + } + } + + result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, None) + + self.assertEqual(expected, result) + + def test_build_filter_min_date_pos_utc(self): + expected = { + 'eventCreateDate': { + 'operation': 'greaterThanDate', + 'options': [ + { + 'name': 'date', + 'value': [ + '2017-10-30T00:00:00.000000+05:00' + ] + } + ] + } + } + + result = self.event_log.build_filter('10/30/2017', None, None, None, None, '+0500') + + self.assertEqual(expected, result) + + def test_build_filter_max_date_pos_utc(self): + expected = { + 'eventCreateDate': { + 'operation': 'lessThanDate', + 'options': [ + { + 'name': 'date', + 'value': [ + '2017-10-31T00:00:00.000000+05:00' + ] + } + ] + } + } + + result = self.event_log.build_filter(None, '10/31/2017', None, None, None, '+0500') + + self.assertEqual(expected, result) + + def test_build_filter_min_max_date_pos_utc(self): + expected = { + 'eventCreateDate': { + 'operation': 'betweenDate', + 'options': [ + { + 'name': 'startDate', + 'value': [ + '2017-10-30T00:00:00.000000+05:00' + ] + }, + { + 'name': 'endDate', + 'value': [ + '2017-10-31T00:00:00.000000+05:00' + ] + } + ] + } + } + + result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, '+0500') + + self.assertEqual(expected, result) + + def test_build_filter_min_date_neg_utc(self): + expected = { + 'eventCreateDate': { + 'operation': 'greaterThanDate', + 'options': [ + { + 'name': 'date', + 'value': [ + '2017-10-30T00:00:00.000000-03:00' + ] + } + ] + } + } + + result = self.event_log.build_filter('10/30/2017', None, None, None, None, '-0300') + + self.assertEqual(expected, result) + + def test_build_filter_max_date_neg_utc(self): + expected = { + 'eventCreateDate': { + 'operation': 'lessThanDate', + 'options': [ + { + 'name': 'date', + 'value': [ + '2017-10-31T00:00:00.000000-03:00' + ] + } + ] + } + } + + result = self.event_log.build_filter(None, '10/31/2017', None, None, None, '-0300') + + self.assertEqual(expected, result) + + def test_build_filter_min_max_date_neg_utc(self): + expected = { + 'eventCreateDate': { + 'operation': 'betweenDate', + 'options': [ + { + 'name': 'startDate', + 'value': [ + '2017-10-30T00:00:00.000000-03:00' + ] + }, + { + 'name': 'endDate', + 'value': [ + '2017-10-31T00:00:00.000000-03:00' + ] + } + ] + } + } + + result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, '-0300') + + self.assertEqual(expected, result) + + def test_build_filter_name(self): + expected = {'eventName': {'operation': 'Add Security Group'}} + + result = self.event_log.build_filter(None, None, 'Add Security Group', None, None, None) + + self.assertEqual(expected, result) + + def test_build_filter_id(self): + expected = {'objectId': {'operation': 1}} + + result = self.event_log.build_filter(None, None, None, 1, None, None) + + self.assertEqual(expected, result) + + def test_build_filter_type(self): + expected = {'objectName': {'operation': 'CCI'}} + + result = self.event_log.build_filter(None, None, None, None, 'CCI', None) + + self.assertEqual(expected, result) diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index add6389fa..5d9a9c597 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -10,6 +10,7 @@ import SoftLayer + from SoftLayer import fixtures from SoftLayer import managers from SoftLayer import testing @@ -17,7 +18,7 @@ MINIMAL_TEST_CREATE_ARGS = { 'size': 'S1270_8GB_2X1TBSATA_NORAID', - 'hostname': 'unicorn', + 'hostnames': ['unicorn'], 'domain': 'giggles.woo', 'location': 'wdc01', 'os': 'UBUNTU_14_64', @@ -176,7 +177,7 @@ def test_generate_create_dict_no_regions(self): def test_generate_create_dict_invalid_size(self): args = { 'size': 'UNKNOWN_SIZE', - 'hostname': 'unicorn', + 'hostnames': ['unicorn'], 'domain': 'giggles.woo', 'location': 'wdc01', 'os': 'UBUNTU_14_64', @@ -190,7 +191,7 @@ def test_generate_create_dict_invalid_size(self): def test_generate_create_dict(self): args = { 'size': 'S1270_8GB_2X1TBSATA_NORAID', - 'hostname': 'unicorn', + 'hostnames': ['unicorn'], 'domain': 'giggles.woo', 'location': 'wdc01', 'os': 'UBUNTU_14_64', @@ -219,6 +220,7 @@ def test_generate_create_dict(self): 'useHourlyPricing': True, 'provisionScripts': ['http://example.com/script.php'], 'sshKeys': [{'sshKeyIds': [10]}], + 'quantity': 1, } data = self.hardware._generate_create_dict(**args) @@ -288,7 +290,7 @@ def test_cancel_hardware_no_billing_item(self): 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') @@ -320,6 +322,14 @@ def test_cancel_hardware_monthly_whenever(self): self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', identifier=6327, args=(False, False, 'No longer needed', '')) + def test_cancel_running_transaction(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = {'id': 987, 'billingItem': {'id': 6327}, + 'activeTransaction': {'id': 4567}} + self.assertRaises(SoftLayer.SoftLayerError, + self.hardware.cancel_hardware, + 12345) + def test_change_port_speed_public(self): self.hardware.change_port_speed(2, True, 100) @@ -392,14 +402,122 @@ def test_update_firmware_selective(self): 'createFirmwareUpdateTransaction', identifier=100, args=(0, 1, 1, 0)) + def test_reflash_firmware(self): + result = self.hardware.reflash_firmware(100) + + self.assertEqual(result, True) + self.assert_called_with('SoftLayer_Hardware_Server', + 'createFirmwareReflashTransaction', + identifier=100, args=(1, 1, 1)) + + def test_reflash_firmware_selective(self): + result = self.hardware.reflash_firmware(100, + raid_controller=False, + bios=False) + + self.assertEqual(result, True) + self.assert_called_with('SoftLayer_Hardware_Server', + 'createFirmwareReflashTransaction', + identifier=100, args=(1, 0, 0)) + + def test_get_tracking_id(self): + result = self.hardware.get_tracking_id(1234) + self.assert_called_with('SoftLayer_Hardware_Server', 'getMetricTrackingObjectId') + self.assertEqual(result, 1000) + + def test_get_bandwidth_data(self): + result = self.hardware.get_bandwidth_data(1234, '2019-01-01', '2019-02-01', 'public', 1000) + self.assert_called_with('SoftLayer_Metric_Tracking_Object', + 'getBandwidthData', + args=('2019-01-01', '2019-02-01', 'public', 1000), + identifier=1000) + self.assertEqual(result[0]['type'], 'cpu0') + + def test_get_bandwidth_allocation(self): + result = self.hardware.get_bandwidth_allocation(1234) + self.assert_called_with('SoftLayer_Hardware_Server', 'getBandwidthAllotmentDetail', identifier=1234) + self.assert_called_with('SoftLayer_Hardware_Server', 'getBillingCycleBandwidthUsage', identifier=1234) + self.assertEqual(result['allotment']['amount'], '250') + self.assertEqual(result['useage'][0]['amountIn'], '.448') + class HardwareHelperTests(testing.TestCase): def test_get_extra_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_extra_price_id, [], 'test', True, None) - self.assertEqual("Could not find valid price for extra option, 'test'", - str(ex)) + self.assertEqual("Could not find valid price for extra option, 'test'", str(ex)) + + def test_get_extra_price_mismatched(self): + items = [ + {'keyName': 'TEST', 'prices': [{'id': 1, 'locationGroupId': None, 'recurringFee': 99}]}, + {'keyName': 'TEST', 'prices': [{'id': 2, 'locationGroupId': 55, 'hourlyRecurringFee': 99}]}, + {'keyName': 'TEST', 'prices': [{'id': 3, 'locationGroupId': None, 'hourlyRecurringFee': 99}]}, + ] + location = { + 'location': { + 'location': { + 'priceGroups': [ + {'id': 50}, + {'id': 51} + ] + } + } + } + result = managers.hardware._get_extra_price_id(items, 'TEST', True, location) + self.assertEqual(3, result) + + def test_get_bandwidth_price_mismatched(self): + items = [ + {'itemCategory': {'categoryCode': 'bandwidth'}, + 'capacity': 100, + 'prices': [{'id': 1, 'locationGroupId': None, 'hourlyRecurringFee': 99}] + }, + {'itemCategory': {'categoryCode': 'bandwidth'}, + 'capacity': 100, + 'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}] + }, + {'itemCategory': {'categoryCode': 'bandwidth'}, + 'capacity': 100, + 'prices': [{'id': 3, 'locationGroupId': None, 'recurringFee': 99}] + }, + ] + location = { + 'location': { + 'location': { + 'priceGroups': [ + {'id': 50}, + {'id': 51} + ] + } + } + } + result = managers.hardware._get_bandwidth_price_id(items, False, False, location) + self.assertEqual(3, result) + + def test_get_os_price_mismatched(self): + items = [ + {'itemCategory': {'categoryCode': 'os'}, + 'softwareDescription': {'referenceCode': 'TEST_OS'}, + 'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}] + }, + {'itemCategory': {'categoryCode': 'os'}, + 'softwareDescription': {'referenceCode': 'TEST_OS'}, + 'prices': [{'id': 3, 'locationGroupId': None, 'recurringFee': 99}] + }, + ] + location = { + 'location': { + 'location': { + 'priceGroups': [ + {'id': 50}, + {'id': 51} + ] + } + } + } + result = managers.hardware._get_os_price_id(items, 'TEST_OS', location) + self.assertEqual(3, result) def test_get_default_price_id_item_not_first(self): items = [{ @@ -414,33 +532,84 @@ def test_get_default_price_id_item_not_first(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_default_price_id, items, 'unknown', True, None) - self.assertEqual("Could not find valid price for 'unknown' option", - str(ex)) + self.assertEqual("Could not find valid price for 'unknown' option", str(ex)) def test_get_default_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_default_price_id, [], 'test', True, None) - self.assertEqual("Could not find valid price for 'test' option", - str(ex)) + self.assertEqual("Could not find valid price for 'test' option", str(ex)) def test_get_bandwidth_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_bandwidth_price_id, [], hourly=True, no_public=False) - self.assertEqual("Could not find valid price for bandwidth option", - str(ex)) + self.assertEqual("Could not find valid price for bandwidth option", str(ex)) def test_get_os_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_os_price_id, [], 'UBUNTU_14_64', None) - self.assertEqual("Could not find valid price for os: 'UBUNTU_14_64'", - str(ex)) + self.assertEqual("Could not find valid price for os: 'UBUNTU_14_64'", str(ex)) def test_get_port_speed_price_id_no_items(self): ex = self.assertRaises(SoftLayer.SoftLayerError, managers.hardware._get_port_speed_price_id, [], 10, True, None) - self.assertEqual("Could not find valid price for port speed: '10'", - str(ex)) + self.assertEqual("Could not find valid price for port speed: '10'", str(ex)) + + def test_get_port_speed_price_id_mismatch(self): + items = [ + {'itemCategory': {'categoryCode': 'port_speed'}, + 'capacity': 101, + 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}], + 'prices': [{'id': 1, 'locationGroupId': None, 'recurringFee': 99}] + }, + {'itemCategory': {'categoryCode': 'port_speed'}, + 'capacity': 100, + 'attributes': [{'attributeTypeKeyName': 'IS_NOT_PRIVATE_NETWORK_ONLY'}], + 'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}] + }, + {'itemCategory': {'categoryCode': 'port_speed'}, + 'capacity': 100, + 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}, {'attributeTypeKeyName': 'NON_LACP'}], + 'prices': [{'id': 3, 'locationGroupId': 55, 'recurringFee': 99}] + }, + {'itemCategory': {'categoryCode': 'port_speed'}, + 'capacity': 100, + 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}], + 'prices': [{'id': 4, 'locationGroupId': 12, 'recurringFee': 99}] + }, + {'itemCategory': {'categoryCode': 'port_speed'}, + 'capacity': 100, + 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}], + 'prices': [{'id': 5, 'locationGroupId': None, 'recurringFee': 99}] + }, + ] + location = { + 'location': { + 'location': { + 'priceGroups': [ + {'id': 50}, + {'id': 51} + ] + } + } + } + result = managers.hardware._get_port_speed_price_id(items, 100, True, location) + self.assertEqual(5, result) + + def test_matches_location(self): + price = {'id': 1, 'locationGroupId': 51, 'recurringFee': 99} + location = { + 'location': { + 'location': { + 'priceGroups': [ + {'id': 50}, + {'id': 51} + ] + } + } + } + result = managers.hardware._matches_location(price, location) + self.assertTrue(result) diff --git a/tests/managers/image_tests.py b/tests/managers/image_tests.py index 5347d4e71..b36deea75 100644 --- a/tests/managers/image_tests.py +++ b/tests/managers/image_tests.py @@ -145,6 +145,34 @@ def test_import_image(self): '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_crn='some_root_key_crn', + wrapped_dek='some_dek', + 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', + 'crkCrn': 'some_root_key_crn', + 'wrappedDek': 'some_dek', + 'cloudInit': False, + 'byol': False, + 'isEncrypted': False + },)) + def test_export_image(self): self.image.export_image_to_uri(1234, 'someuri') @@ -153,3 +181,14 @@ def test_export_image(self): '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 --git a/tests/managers/network_tests.py b/tests/managers/network_tests.py index 7a84bebae..a87441a99 100644 --- a/tests/managers/network_tests.py +++ b/tests/managers/network_tests.py @@ -4,6 +4,10 @@ :license: MIT, see LICENSE for more details. """ +import mock +import sys +import unittest + import SoftLayer from SoftLayer import fixtures from SoftLayer.managers import network @@ -24,9 +28,6 @@ def test_ip_lookup(self): 'getByIpAddress', args=('10.0.1.37',)) - def test_add_subnet_raises_exception_on_failure(self): - self.assertRaises(TypeError, self.network.add_subnet, ('bad')) - def test_add_global_ip(self): # Test a global IPv4 order result = self.network.add_global_ip(test_order=True) @@ -80,7 +81,7 @@ def test_add_subnet_for_ipv4(self): # Test a four public address IPv4 order result = self.network.add_subnet('public', quantity=4, - vlan_id=1234, + endpoint_id=1234, version=4, test_order=True) @@ -88,11 +89,11 @@ def test_add_subnet_for_ipv4(self): result = self.network.add_subnet('public', quantity=4, - vlan_id=1234, + endpoint_id=1234, version=4, test_order=False) - self.assertEqual(fixtures.SoftLayer_Product_Order.verifyOrder, result) + self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) result = self.network.add_subnet('global', test_order=True) @@ -103,7 +104,7 @@ def test_add_subnet_for_ipv6(self): # Test a public IPv6 order result = self.network.add_subnet('public', quantity=64, - vlan_id=45678, + endpoint_id=45678, version=6, test_order=True) @@ -466,3 +467,161 @@ def test_unassign_global_ip(self): self.assert_called_with('SoftLayer_Network_Subnet_IpAddress_Global', 'unroute', identifier=9876) + + def test_get_event_logs_by_request_id(self): + expected = [ + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T09:40:32.238869-05:00', + 'eventName': 'Security Group Added', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '{"securityGroupId":"200",' + '"securityGroupName":"test_SG",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public",' + '"requestId":"96c9b47b9e102d2e1d81fba"}', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '59e767e03a57e', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:42:13.089536-05:00', + 'eventName': 'Security Group Rule(s) Removed', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"96c9b47b9e102d2e1d81fba",' + '"rules":[{"ruleId":"800",' + '"remoteIp":null,"remoteGroupId":null,"direction":"ingress",' + '"ethertype":"IPv4",' + '"portRangeMin":2000,"portRangeMax":2001,"protocol":"tcp"}]}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e7765515e28', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + } + ] + + with mock.patch.object(self.network, '_get_cci_event_logs') as cci_mock: + with mock.patch.object(self.network, '_get_security_group_event_logs') as sg_mock: + cci_mock.return_value = [ + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T09:40:32.238869-05:00', + 'eventName': 'Security Group Added', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '{"securityGroupId":"200",' + '"securityGroupName":"test_SG",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public",' + '"requestId":"96c9b47b9e102d2e1d81fba"}', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '59e767e03a57e', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-23T14:22:36.221541-05:00', + 'eventName': 'Disable Port', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '100', + 'userId': '', + 'userType': 'SYSTEM' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T09:40:41.830338-05:00', + 'eventName': 'Security Group Rule Added', + 'ipAddress': '192.168.0.1', + 'label': 'test.softlayer.com', + 'metaData': '{"securityGroupId":"200",' + '"securityGroupName":"test_SG",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public",' + '"requestId":"53d0b91d392864e062f4958",' + '"rules":[{"ruleId":"100",' + '"remoteIp":null,"remoteGroupId":null,"direction":"ingress",' + '"ethertype":"IPv4",' + '"portRangeMin":2000,"portRangeMax":2001,"protocol":"tcp"}]}', + 'objectId': 300, + 'objectName': 'CCI', + 'traceId': '59e767e9c2184', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + } + ] + + sg_mock.return_value = [ + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:42:13.089536-05:00', + 'eventName': 'Security Group Rule(s) Removed', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"96c9b47b9e102d2e1d81fba",' + '"rules":[{"ruleId":"800",' + '"remoteIp":null,"remoteGroupId":null,"direction":"ingress",' + '"ethertype":"IPv4",' + '"portRangeMin":2000,"portRangeMax":2001,"protocol":"tcp"}]}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e7765515e28', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + }, + { + 'accountId': 100, + 'eventCreateDate': '2017-10-18T10:42:11.679736-05:00', + 'eventName': 'Network Component Removed from Security Group', + 'ipAddress': '192.168.0.1', + 'label': 'test_SG', + 'metaData': '{"requestId":"6b9a87a9ab8ac9a22e87a00",' + '"fullyQualifiedDomainName":"test.softlayer.com",' + '"networkComponentId":"100",' + '"networkInterfaceType":"public"}', + 'objectId': 700, + 'objectName': 'Security Group', + 'traceId': '59e77653a1e5f', + 'userId': 400, + 'userType': 'CUSTOMER', + 'username': 'user' + } + ] + + result = self.network.get_event_logs_by_request_id('96c9b47b9e102d2e1d81fba') + + self.assertEqual(expected, result) + + @unittest.skipIf(sys.version_info < (3, 6), "__next__ doesn't work in python 2") + def test_get_security_group_event_logs(self): + result = self.network._get_security_group_event_logs() + # Event log now returns a generator, so you have to get a result for it to make an API call + log = result.__next__() + _filter = {'objectName': {'operation': 'Security Group'}} + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=_filter) + self.assertEqual(100, log['accountId']) + + @unittest.skipIf(sys.version_info < (3, 6), "__next__ doesn't work in python 2") + def test_get_cci_event_logs(self): + result = self.network._get_cci_event_logs() + # Event log now returns a generator, so you have to get a result for it to make an API call + log = result.__next__() + _filter = {'objectName': {'operation': 'CCI'}} + self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=_filter) + self.assertEqual(100, log['accountId']) diff --git a/tests/managers/object_storage_tests.py b/tests/managers/object_storage_tests.py index 478e5158b..e5042080d 100644 --- a/tests/managers/object_storage_tests.py +++ b/tests/managers/object_storage_tests.py @@ -42,3 +42,80 @@ def test_list_endpoints_no_results(self): endpoints = self.object_storage.list_endpoints() self.assertEqual(endpoints, []) + + def test_create_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'credentialCreate') + accounts.return_value = { + "id": 1103123, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXMkAF", + "username": "XfHhBNBPlPdlWyaP", + "type": { + "name": "S3 Compatible Signature" + } + } + credential = self.object_storage.create_credential(100) + self.assertEqual(credential, + { + "id": 1103123, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXMkAF", + "username": "XfHhBNBPlPdlWyaP", + "type": { + "name": "S3 Compatible Signature" + } + }) + + def test_delete_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'credentialDelete') + accounts.return_value = True + + credential = self.object_storage.delete_credential(100) + self.assertEqual(credential, True) + + def test_limit_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'getCredentialLimit') + accounts.return_value = 2 + + credential = self.object_storage.limit_credential(100) + self.assertEqual(credential, 2) + + def test_list_credential(self): + accounts = self.set_mock('SoftLayer_Network_Storage_Hub_Cleversafe_Account', 'getCredentials') + accounts.return_value = [ + { + "id": 1103123, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXsf4sf", + "username": "XfHhBNBPlPdlWyaP3fsd", + "type": { + "name": "S3 Compatible Signature" + } + }, + { + "id": 1102341, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXMkAF", + "username": "XfHhBNBPlPdlWyaP", + "type": { + "name": "S3 Compatible Signature" + } + } + ] + credential = self.object_storage.list_credential(100) + self.assertEqual(credential, + [ + { + "id": 1103123, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXsf4sf", + "username": "XfHhBNBPlPdlWyaP3fsd", + "type": { + "name": "S3 Compatible Signature" + } + }, + { + "id": 1102341, + "password": "nwUEUsx6PiEoN0B1Xe9z9hUCyXMkAF", + "username": "XfHhBNBPlPdlWyaP", + "type": { + "name": "S3 Compatible Signature" + } + } + ] + ) diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py index 729659ba6..40f9f5db3 100644 --- a/tests/managers/ordering_tests.py +++ b/tests/managers/ordering_tests.py @@ -68,6 +68,18 @@ def test_get_package_id_by_type_returns_valid_id(self): 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 = [] @@ -78,9 +90,8 @@ def test_get_package_id_by_type_fails_for_nonexistent_package_type(self): def test_get_order_container(self): container = self.ordering.get_order_container(1234) - quote = self.ordering.client['Billing_Order_Quote'] - container_fixture = quote.getRecalculatedOrderContainer(id=1234) - self.assertEqual(container, container_fixture['orderContainers'][0]) + self.assertEqual(1, container['quantity']) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') def test_get_quotes(self): quotes = self.ordering.get_quotes() @@ -94,13 +105,16 @@ def test_get_quote_details(self): self.assertEqual(quote, quote_fixture) def test_verify_quote(self): - result = self.ordering.verify_quote(1234, - [{'hostname': 'test1', - 'domain': 'example.com'}], - quantity=1) + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }] + } + result = self.ordering.verify_quote(1234, extras) - self.assertEqual(result, fixtures.SoftLayer_Product_Order.verifyOrder) - self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + self.assertEqual(result, fixtures.SoftLayer_Billing_Order_Quote.verifyOrder) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder') def test_order_quote_virtual_guest(self): guest_quote = { @@ -114,38 +128,37 @@ def test_order_quote_virtual_guest(self): 'useHourlyPricing': '', }], } - + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }] + } mock = self.set_mock('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') mock.return_value = guest_quote - result = self.ordering.order_quote(1234, - [{'hostname': 'test1', - 'domain': 'example.com'}], - quantity=1) + result = self.ordering.order_quote(1234, extras) - self.assertEqual(result, fixtures.SoftLayer_Product_Order.placeOrder) - self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + self.assertEqual(result, fixtures.SoftLayer_Billing_Order_Quote.placeOrder) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'placeOrder') def test_generate_order_template(self): - result = self.ordering.generate_order_template( - 1234, [{'hostname': 'test1', 'domain': 'example.com'}], quantity=1) - self.assertEqual(result, {'presetId': None, - 'hardware': [{'domain': 'example.com', - 'hostname': 'test1'}], - 'useHourlyPricing': '', - 'packageId': 50, - 'prices': [{'id': 1921}], - 'quantity': 1}) + extras = {'hardware': [{'hostname': 'test1', 'domain': 'example.com'}]} + + result = self.ordering.generate_order_template(1234, extras, quantity=1) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') + self.assertEqual(result['hardware'][0]['domain'], 'example.com') def test_generate_order_template_virtual(self): - result = self.ordering.generate_order_template( - 1234, [{'hostname': 'test1', 'domain': 'example.com'}], quantity=1) - self.assertEqual(result, {'presetId': None, - 'hardware': [{'domain': 'example.com', - 'hostname': 'test1'}], - 'useHourlyPricing': '', - 'packageId': 50, - 'prices': [{'id': 1921}], - 'quantity': 1}) + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }], + 'testProperty': 100 + } + result = self.ordering.generate_order_template(1234, extras, quantity=1) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') + self.assertEqual(result['testProperty'], 100) def test_generate_order_template_extra_quantity(self): self.assertRaises(ValueError, @@ -284,7 +297,8 @@ def test_get_preset_by_key_preset_not_found(self): def test_get_price_id_list(self): category1 = {'categoryCode': 'cat1'} - price1 = {'id': 1234, 'locationGroupId': None, 'itemCategory': [category1]} + 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]} @@ -293,7 +307,7 @@ def test_get_price_id_list(self): 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, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) @@ -308,7 +322,7 @@ def test_get_price_id_list_item_not_found(self): exc = self.assertRaises(exceptions.SoftLayerError, self.ordering.get_price_id_list, - 'PACKAGE_KEYNAME', ['ITEM2']) + '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)) @@ -321,7 +335,7 @@ def test_get_price_id_list_gpu_items_with_two_categories(self): 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']) + 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) @@ -354,7 +368,37 @@ def test_generate_order_with_preset(self): 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_with_quantity(self): + pkg = 'PACKAGE_KEYNAME' + quantity = 2 + items = ['ITEM1', 'ITEM2'] + extras = {"hardware": [{"hostname": "test01", "domain": "example.com"}, + {"hostname": "test02", "domain": "example.com"}]} + complex_type = 'My_Type' + expected_order = {'orderContainers': [ + {'complexType': 'My_Type', + 'hardware': [{'domain': 'example.com', + 'hostname': 'test01'}, + {'domain': 'example.com', + 'hostname': 'test02'}], + 'location': 1854895, + 'packageId': 1234, + 'prices': [{'id': 1111}, {'id': 2222}], + 'quantity': 2, + 'useHourlyPricing': True} + ]} + + mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() + + order = self.ordering.generate_order(pkg, 'DALLAS13', items, complex_type=complex_type, quantity=quantity, + extras=extras) + + mock_pkg.assert_called_once_with(pkg, mask='id') + mock_preset.assert_not_called() + mock_get_ids.assert_called_once_with(pkg, items, None) self.assertEqual(expected_order, order) def test_generate_order(self): @@ -376,7 +420,7 @@ def test_generate_order(self): 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): @@ -431,6 +475,60 @@ def test_place_order(self): extras=extras, quantity=quantity) self.assertEqual(ord_mock.return_value, order) + def test_place_order_with_quantity(self): + ord_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + ord_mock.return_value = {'id': 1234} + pkg = 'PACKAGE_KEYNAME' + location = 'DALLAS13' + items = ['ITEM1', 'ITEM2'] + hourly = True + preset_keyname = 'PRESET' + complex_type = 'Complex_Type' + extras = {"hardware": [{"hostname": "test01", "domain": "example.com"}, + {"hostname": "test02", "domain": "example.com"}]} + quantity = 2 + + with mock.patch.object(self.ordering, 'generate_order') as gen_mock: + gen_mock.return_value = {'order': {}} + + order = self.ordering.place_order(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + complex_type=complex_type, + extras=extras, quantity=quantity) + + 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_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']) @@ -469,7 +567,7 @@ def test_get_location_id_keyname(self): 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) @@ -487,7 +585,7 @@ def test_location_group_id_none(self): 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, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) @@ -504,7 +602,73 @@ def test_location_groud_id_empty(self): 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, 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) + + def test_clean_quote_verify(self): + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }], + 'testProperty': '' + } + result = self.ordering.verify_quote(1234, extras) + + self.assertEqual(result, fixtures.SoftLayer_Billing_Order_Quote.verifyOrder) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder') + call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder')[0] + order_container = call.args[0] + self.assertNotIn('testProperty', order_container) + self.assertNotIn('reservedCapacityId', order_container) diff --git a/tests/managers/queue_tests.py b/tests/managers/queue_tests.py deleted file mode 100644 index beecaf616..000000000 --- a/tests/managers/queue_tests.py +++ /dev/null @@ -1,467 +0,0 @@ -""" - SoftLayer.tests.managers.queue_tests - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - :license: MIT, see LICENSE for more details. -""" -import mock - -import SoftLayer -from SoftLayer import consts -from SoftLayer.managers import messaging -from SoftLayer import testing - -QUEUE_1 = { - 'expiration': 40000, - 'message_count': 0, - 'name': 'example_queue', - 'tags': ['tag1', 'tag2', 'tag3'], - 'visibility_interval': 10, - 'visible_message_count': 0} -QUEUE_LIST = {'item_count': 1, 'items': [QUEUE_1]} -MESSAGE_1 = { - 'body': '', - 'fields': {'field': 'value'}, - 'id': 'd344a01133b61181f57d9950a852eb10', - 'initial_entry_time': 1343402631.3917992, - 'message': 'Object created', - 'visibility_delay': 0, - 'visibility_interval': 30000} -MESSAGE_POP = { - 'item_count': 1, - 'items': [MESSAGE_1], -} -MESSAGE_POP_EMPTY = { - 'item_count': 0, - 'items': [] -} - -TOPIC_1 = {'name': 'example_topic', 'tags': ['tag1', 'tag2', 'tag3']} -TOPIC_LIST = {'item_count': 1, 'items': [TOPIC_1]} -SUBSCRIPTION_1 = { - 'endpoint': { - 'account_id': 'test', - 'queue_name': 'topic_subscription_queue'}, - 'endpoint_type': 'queue', - 'id': 'd344a01133b61181f57d9950a85704d4', - 'message': 'Object created'} -SUBSCRIPTION_LIST = {'item_count': 1, 'items': [SUBSCRIPTION_1]} - - -def mocked_auth_call(self): - self.auth_token = 'NEW_AUTH_TOKEN' - - -class QueueAuthTests(testing.TestCase): - def set_up(self): - self.auth = messaging.QueueAuth( - 'endpoint', 'username', 'api_key', auth_token='auth_token') - - def test_init(self): - auth = SoftLayer.managers.messaging.QueueAuth( - 'endpoint', 'username', 'api_key', auth_token='auth_token') - self.assertEqual(auth.endpoint, 'endpoint') - self.assertEqual(auth.username, 'username') - self.assertEqual(auth.api_key, 'api_key') - self.assertEqual(auth.auth_token, 'auth_token') - - @mock.patch('SoftLayer.managers.messaging.requests.post') - def test_auth(self, post): - post().headers = {'X-Auth-Token': 'NEW_AUTH_TOKEN'} - post().ok = True - self.auth.auth() - self.auth.auth_token = 'NEW_AUTH_TOKEN' - - post().ok = False - self.assertRaises(SoftLayer.Unauthenticated, self.auth.auth) - - @mock.patch('SoftLayer.managers.messaging.QueueAuth.auth', - mocked_auth_call) - def test_handle_error_200(self): - # No op on no error - request = mock.MagicMock() - request.status_code = 200 - self.auth.handle_error(request) - - self.assertEqual(self.auth.auth_token, 'auth_token') - self.assertFalse(request.request.send.called) - - @mock.patch('SoftLayer.managers.messaging.QueueAuth.auth', - mocked_auth_call) - def test_handle_error_503(self): - # Retry once more on 503 error - request = mock.MagicMock() - request.status_code = 503 - self.auth.handle_error(request) - - self.assertEqual(self.auth.auth_token, 'auth_token') - request.connection.send.assert_called_with(request.request) - - @mock.patch('SoftLayer.managers.messaging.QueueAuth.auth', - mocked_auth_call) - def test_handle_error_401(self): - # Re-auth on 401 - request = mock.MagicMock() - request.status_code = 401 - request.request.headers = {'X-Auth-Token': 'OLD_AUTH_TOKEN'} - self.auth.handle_error(request) - - self.assertEqual(self.auth.auth_token, 'NEW_AUTH_TOKEN') - request.connection.send.assert_called_with(request.request) - - @mock.patch('SoftLayer.managers.messaging.QueueAuth.auth', - mocked_auth_call) - def test_call_unauthed(self): - request = mock.MagicMock() - request.headers = {} - self.auth.auth_token = None - self.auth(request) - - self.assertEqual(self.auth.auth_token, 'NEW_AUTH_TOKEN') - request.register_hook.assert_called_with( - 'response', self.auth.handle_error) - self.assertEqual(request.headers, {'X-Auth-Token': 'NEW_AUTH_TOKEN'}) - - -class MessagingManagerTests(testing.TestCase): - - def set_up(self): - self.client = mock.MagicMock() - self.manager = SoftLayer.MessagingManager(self.client) - - def test_list_accounts(self): - self.manager.list_accounts() - self.client['Account'].getMessageQueueAccounts.assert_called_with( - mask=mock.ANY) - - def test_get_endpoints(self): - endpoints = self.manager.get_endpoints() - self.assertEqual(endpoints, SoftLayer.managers.messaging.ENDPOINTS) - - @mock.patch('SoftLayer.managers.messaging.ENDPOINTS', { - 'datacenter01': { - 'private': 'private_endpoint', 'public': 'public_endpoint'}, - 'dal05': { - 'private': 'dal05_private', 'public': 'dal05_public'}}) - def test_get_endpoint(self): - # Defaults to dal05, public - endpoint = self.manager.get_endpoint() - self.assertEqual(endpoint, 'https://dal05_public') - - endpoint = self.manager.get_endpoint(network='private') - self.assertEqual(endpoint, 'https://dal05_private') - - endpoint = self.manager.get_endpoint(datacenter='datacenter01') - self.assertEqual(endpoint, 'https://public_endpoint') - - endpoint = self.manager.get_endpoint(datacenter='datacenter01', - network='private') - self.assertEqual(endpoint, 'https://private_endpoint') - - endpoint = self.manager.get_endpoint(datacenter='datacenter01', - network='private') - self.assertEqual(endpoint, 'https://private_endpoint') - - # ERROR CASES - self.assertRaises( - TypeError, - self.manager.get_endpoint, datacenter='doesnotexist') - - self.assertRaises( - TypeError, - self.manager.get_endpoint, network='doesnotexist') - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection') - def test_get_connection(self, conn): - queue_conn = self.manager.get_connection('QUEUE_ACCOUNT_ID') - conn.assert_called_with( - 'QUEUE_ACCOUNT_ID', endpoint='https://dal05.mq.softlayer.net') - conn().authenticate.assert_called_with( - self.client.auth.username, self.client.auth.api_key) - self.assertEqual(queue_conn, conn()) - - def test_get_connection_no_auth(self): - self.client.auth = None - self.assertRaises(SoftLayer.SoftLayerError, - self.manager.get_connection, 'QUEUE_ACCOUNT_ID') - - def test_get_connection_no_username(self): - self.client.auth.username = None - self.assertRaises(SoftLayer.SoftLayerError, - self.manager.get_connection, 'QUEUE_ACCOUNT_ID') - - def test_get_connection_no_api_key(self): - self.client.auth.api_key = None - self.assertRaises(SoftLayer.SoftLayerError, - self.manager.get_connection, 'QUEUE_ACCOUNT_ID') - - @mock.patch('SoftLayer.managers.messaging.requests.get') - def test_ping(self, get): - result = self.manager.ping() - - get.assert_called_with('https://dal05.mq.softlayer.net/v1/ping') - get().raise_for_status.assert_called_with() - self.assertTrue(result) - - -class MessagingConnectionTests(testing.TestCase): - - def set_up(self): - self.conn = SoftLayer.managers.messaging.MessagingConnection( - 'acount_id', endpoint='endpoint') - self.auth = mock.MagicMock() - self.conn.auth = self.auth - - def test_init(self): - self.assertEqual(self.conn.account_id, 'acount_id') - self.assertEqual(self.conn.endpoint, 'endpoint') - self.assertEqual(self.conn.auth, self.auth) - - @mock.patch('SoftLayer.managers.messaging.requests.request') - def test_make_request(self, request): - resp = self.conn._make_request('GET', 'path') - request.assert_called_with( - 'GET', 'endpoint/v1/acount_id/path', - headers={ - 'Content-Type': 'application/json', - 'User-Agent': consts.USER_AGENT}, - auth=self.auth) - request().raise_for_status.assert_called_with() - self.assertEqual(resp, request()) - - @mock.patch('SoftLayer.managers.messaging.QueueAuth') - def test_authenticate(self, auth): - self.conn.authenticate('username', 'api_key', auth_token='auth_token') - - auth.assert_called_with( - 'endpoint/v1/acount_id/auth', 'username', 'api_key', - auth_token='auth_token') - auth().auth.assert_called_with() - self.assertEqual(self.conn.auth, auth()) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_stats(self, make_request): - content = { - 'notifications': [{'key': [2012, 7, 27, 14, 31], 'value': 2}], - 'requests': [{'key': [2012, 7, 27, 14, 31], 'value': 11}]} - make_request().json.return_value = content - result = self.conn.stats() - - make_request.assert_called_with('get', 'stats/hour') - self.assertEqual(content, result) - - # Queue-based Tests - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_get_queues(self, make_request): - make_request().json.return_value = QUEUE_LIST - result = self.conn.get_queues() - - make_request.assert_called_with('get', 'queues', params={}) - self.assertEqual(QUEUE_LIST, result) - - # with tags - result = self.conn.get_queues(tags=['tag1', 'tag2']) - - make_request.assert_called_with( - 'get', 'queues', params={'tags': 'tag1,tag2'}) - self.assertEqual(QUEUE_LIST, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_create_queue(self, make_request): - make_request().json.return_value = QUEUE_1 - result = self.conn.create_queue('example_queue') - - make_request.assert_called_with( - 'put', 'queues/example_queue', data='{}') - self.assertEqual(QUEUE_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_modify_queue(self, make_request): - make_request().json.return_value = QUEUE_1 - result = self.conn.modify_queue('example_queue') - - make_request.assert_called_with( - 'put', 'queues/example_queue', data='{}') - self.assertEqual(QUEUE_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_get_queue(self, make_request): - make_request().json.return_value = QUEUE_1 - result = self.conn.get_queue('example_queue') - - make_request.assert_called_with('get', 'queues/example_queue') - self.assertEqual(QUEUE_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_delete_queue(self, make_request): - result = self.conn.delete_queue('example_queue') - make_request.assert_called_with( - 'delete', 'queues/example_queue', params={}) - self.assertTrue(result) - - # With Force - result = self.conn.delete_queue('example_queue', force=True) - make_request.assert_called_with( - 'delete', 'queues/example_queue', params={'force': 1}) - self.assertTrue(result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_push_queue_message(self, make_request): - make_request().json.return_value = MESSAGE_1 - result = self.conn.push_queue_message('example_queue', '') - - make_request.assert_called_with( - 'post', 'queues/example_queue/messages', data='{"body": ""}') - self.assertEqual(MESSAGE_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_pop_messages(self, make_request): - make_request().json.return_value = MESSAGE_POP - result = self.conn.pop_messages('example_queue') - - make_request.assert_called_with( - 'get', 'queues/example_queue/messages', params={'batch': 1}) - self.assertEqual(MESSAGE_POP, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_pop_message(self, make_request): - make_request().json.return_value = MESSAGE_POP - result = self.conn.pop_message('example_queue') - - make_request.assert_called_with( - 'get', 'queues/example_queue/messages', params={'batch': 1}) - self.assertEqual(MESSAGE_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_pop_message_empty(self, make_request): - make_request().json.return_value = MESSAGE_POP_EMPTY - result = self.conn.pop_message('example_queue') - - make_request.assert_called_with( - 'get', 'queues/example_queue/messages', params={'batch': 1}) - self.assertEqual(None, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_delete_message(self, make_request): - result = self.conn.delete_message('example_queue', MESSAGE_1['id']) - - make_request.assert_called_with( - 'delete', 'queues/example_queue/messages/%s' % MESSAGE_1['id']) - self.assertTrue(result) - - # Topic-based Tests - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_get_topics(self, make_request): - make_request().json.return_value = TOPIC_LIST - result = self.conn.get_topics() - - make_request.assert_called_with('get', 'topics', params={}) - self.assertEqual(TOPIC_LIST, result) - - # with tags - result = self.conn.get_topics(tags=['tag1', 'tag2']) - - make_request.assert_called_with( - 'get', 'topics', params={'tags': 'tag1,tag2'}) - self.assertEqual(TOPIC_LIST, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_create_topic(self, make_request): - make_request().json.return_value = TOPIC_1 - result = self.conn.create_topic('example_topic') - - make_request.assert_called_with( - 'put', 'topics/example_topic', data='{}') - self.assertEqual(TOPIC_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_modify_topic(self, make_request): - make_request().json.return_value = TOPIC_1 - result = self.conn.modify_topic('example_topic') - - make_request.assert_called_with( - 'put', 'topics/example_topic', data='{}') - self.assertEqual(TOPIC_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_get_topic(self, make_request): - make_request().json.return_value = TOPIC_1 - result = self.conn.get_topic('example_topic') - - make_request.assert_called_with('get', 'topics/example_topic') - self.assertEqual(TOPIC_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_delete_topic(self, make_request): - result = self.conn.delete_topic('example_topic') - make_request.assert_called_with( - 'delete', 'topics/example_topic', params={}) - self.assertTrue(result) - - # With Force - result = self.conn.delete_topic('example_topic', force=True) - make_request.assert_called_with( - 'delete', 'topics/example_topic', params={'force': 1}) - self.assertTrue(result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_push_topic_message(self, make_request): - make_request().json.return_value = MESSAGE_1 - result = self.conn.push_topic_message('example_topic', '') - - make_request.assert_called_with( - 'post', 'topics/example_topic/messages', data='{"body": ""}') - self.assertEqual(MESSAGE_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_get_subscriptions(self, make_request): - make_request().json.return_value = SUBSCRIPTION_LIST - result = self.conn.get_subscriptions('example_topic') - - make_request.assert_called_with( - 'get', 'topics/example_topic/subscriptions') - self.assertEqual(SUBSCRIPTION_LIST, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection' - '._make_request') - def test_create_subscription(self, make_request): - make_request().json.return_value = SUBSCRIPTION_1 - endpoint_details = { - 'account_id': 'test', - 'queue_name': 'topic_subscription_queue'} - result = self.conn.create_subscription( - 'example_topic', 'queue', **endpoint_details) - - make_request.assert_called_with( - 'post', 'topics/example_topic/subscriptions', data=mock.ANY) - self.assertEqual(SUBSCRIPTION_1, result) - - @mock.patch('SoftLayer.managers.messaging.MessagingConnection.' - '_make_request') - def test_delete_subscription(self, make_request): - make_request().json.return_value = SUBSCRIPTION_1 - result = self.conn.delete_subscription( - 'example_topic', SUBSCRIPTION_1['id']) - - make_request.assert_called_with( - 'delete', - 'topics/example_topic/subscriptions/%s' % SUBSCRIPTION_1['id']) - self.assertTrue(result) diff --git a/tests/managers/sshkey_tests.py b/tests/managers/sshkey_tests.py index b21d0131f..19a0e2317 100644 --- a/tests/managers/sshkey_tests.py +++ b/tests/managers/sshkey_tests.py @@ -19,7 +19,7 @@ def test_add_key(self): 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 --git a/tests/managers/ticket_tests.py b/tests/managers/ticket_tests.py index 50ed7b29a..ef3638f05 100644 --- a/tests/managers/ticket_tests.py +++ b/tests/managers/ticket_tests.py @@ -72,7 +72,6 @@ def test_create_ticket(self): subject=1004) args = ({"assignedUserId": 12345, - "contents": "body", "subjectId": 1004, "title": "Cloud Instance Cancellation - 08/01/13"}, "body") diff --git a/tests/managers/user_tests.py b/tests/managers/user_tests.py index ddb8322f1..66443de04 100644 --- a/tests/managers/user_tests.py +++ b/tests/managers/user_tests.py @@ -191,3 +191,33 @@ 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) + + def test_create_user_handle_paas_exception(self): + user_template = {"username": "foobar", "email": "foobar@example.com"} + + self.manager.user_service = mock.Mock() + + # FaultCode IS NOT SoftLayer_Exception_User_Customer_DelegateIamIdInvitationToPaas + any_error = exceptions.SoftLayerAPIError("SoftLayer_Exception_User_Customer", + "This exception indicates an error") + + self.manager.user_service.createObject.side_effect = any_error + + try: + self.manager.create_user(user_template, "Pass@123") + except exceptions.SoftLayerAPIError as ex: + self.assertEqual(ex.faultCode, "SoftLayer_Exception_User_Customer") + self.assertEqual(ex.faultString, "This exception indicates an error") + + # FaultCode is SoftLayer_Exception_User_Customer_DelegateIamIdInvitationToPaas + paas_error = exceptions.SoftLayerAPIError("SoftLayer_Exception_User_Customer_DelegateIamIdInvitationToPaas", + "This exception does NOT indicate an error") + + self.manager.user_service.createObject.side_effect = paas_error + + try: + self.manager.create_user(user_template, "Pass@123") + except exceptions.SoftLayerError as ex: + self.assertEqual(ex.args[0], "Your request for a new user was received, but it needs to be processed by " + "the Platform Services API first. Barring any errors on the Platform Services " + "side, your new user should be created shortly.") diff --git a/tests/managers/vs/__init__.py b/tests/managers/vs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/managers/vs/vs_capacity_tests.py b/tests/managers/vs/vs_capacity_tests.py new file mode 100644 index 000000000..5229ebec4 --- /dev/null +++ b/tests/managers/vs/vs_capacity_tests.py @@ -0,0 +1,195 @@ +""" + SoftLayer.tests.managers.vs.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 VSManagerCapacityTests(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_get_available_routers_search(self): + + result = self.manager.get_available_routers('wdc07') + package_filter = {'keyName': {'operation': 'RESERVED_CAPACITY'}} + pod_filter = {'datacenterName': {'operation': 'wdc07'}} + 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', filter=pod_filter) + 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 --git a/tests/managers/vs/vs_order_tests.py b/tests/managers/vs/vs_order_tests.py new file mode 100644 index 000000000..7b54f5450 --- /dev/null +++ b/tests/managers/vs/vs_order_tests.py @@ -0,0 +1,243 @@ +""" + SoftLayer.tests.managers.vs.vs_order_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + These tests deal with ordering in the VS manager. + :license: MIT, see LICENSE for more details. + +""" +import mock + +import SoftLayer +from SoftLayer import exceptions +from SoftLayer import fixtures +from SoftLayer import testing + + +class VSOrderTests(testing.TestCase): + + def set_up(self): + self.vs = SoftLayer.VSManager(self.client) + + @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') + def test_create_verify(self, create_dict): + create_dict.return_value = {'test': 1, 'verify': 1} + + self.vs.verify_create_instance(test=1, verify=1, tags=['test', 'tags']) + + create_dict.assert_called_once_with(test=1, verify=1) + self.assert_called_with('SoftLayer_Virtual_Guest', + 'generateOrderTemplate', + args=({'test': 1, 'verify': 1},)) + + def test_upgrade(self): + # test single upgrade + result = self.vs.upgrade(1, cpus=4, public=False) + + 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(order_container['prices'], [{'id': 1007}]) + self.assertEqual(order_container['virtualGuests'], [{'id': 1}]) + + def test_upgrade_blank(self): + # Now test a blank upgrade + result = self.vs.upgrade(1) + + self.assertEqual(result, False) + self.assertEqual(self.calls('SoftLayer_Product_Order', 'placeOrder'), []) + + def test_upgrade_full(self): + # Testing all parameters Upgrade + result = self.vs.upgrade(1, cpus=4, memory=2, 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.assertIn({'id': 1144}, order_container['prices']) + self.assertIn({'id': 1133}, order_container['prices']) + 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 + + # test single upgrade + result = self.vs.upgrade(1, cpus=4, public=False) + + 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(order_container['prices'], [{'id': 115566}]) + self.assertEqual(order_container['virtualGuests'], [{'id': 1}]) + + def test_get_item_id_for_upgrade(self): + item_id = 0 + package_items = self.client['Product_Package'].getItems(id=46) + for item in package_items: + if ((item['prices'][0]['categories'][0]['id'] == 3) + and (item.get('capacity') == '2')): + item_id = item['prices'][0]['id'] + break + self.assertEqual(1133, item_id) + + def test_get_package_items(self): + self.vs._get_package_items() + self.assert_called_with('SoftLayer_Product_Package', 'getItems') + + def test_get_price_id_for_upgrade(self): + package_items = self.vs._get_package_items() + + price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, + option='cpus', + value='4') + self.assertEqual(1144, price_id) + + def test_get_price_id_for_upgrade_skips_location_price(self): + package_items = self.vs._get_package_items() + + price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, + option='cpus', + value='55') + self.assertEqual(None, price_id) + + def test_get_price_id_for_upgrade_finds_nic_price(self): + package_items = self.vs._get_package_items() + + price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, + option='memory', + value='2') + self.assertEqual(1133, price_id) + + def test_get_price_id_for_upgrade_finds_memory_price(self): + package_items = self.vs._get_package_items() + + price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, + option='nic_speed', + value='1000') + self.assertEqual(1122, price_id) + + def test__get_price_id_for_upgrade_find_private_price(self): + package_items = self.vs._get_package_items() + price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, + option='cpus', + value='4', + public=False) + self.assertEqual(1007, price_id) + + def test_upgrade_mem_and_preset_exception(self): + self.assertRaises( + ValueError, + self.vs.upgrade, + 1234, + memory=10, + preset="M1_64X512X100" + ) + + def test_upgrade_cpu_and_preset_exception(self): + self.assertRaises( + ValueError, + self.vs.upgrade, + 1234, + cpus=10, + preset="M1_64X512X100" + ) + + @mock.patch('SoftLayer.managers.vs.VSManager._get_price_id_for_upgrade_option') + def test_upgrade_no_price_exception(self, get_price): + get_price.return_value = None + self.assertRaises( + exceptions.SoftLayerError, + self.vs.upgrade, + 1234, + memory=1, + ) + + @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') + def test_order_guest(self, create_dict): + create_dict.return_value = {'test': 1, 'verify': 1} + guest = {'test': 1, 'verify': 1, 'tags': ['First']} + result = self.vs.order_guest(guest, test=False) + create_dict.assert_called_once_with(test=1, verify=1) + self.assertEqual(1234, result['orderId']) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate') + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + self.assert_called_with('SoftLayer_Virtual_Guest', 'setTags', identifier=1234567) + + @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') + def test_order_guest_verify(self, create_dict): + create_dict.return_value = {'test': 1, 'verify': 1} + guest = {'test': 1, 'verify': 1, 'tags': ['First']} + result = self.vs.order_guest(guest, test=True) + create_dict.assert_called_once_with(test=1, verify=1) + self.assertEqual(1234, result['orderId']) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate') + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + + @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') + def test_order_guest_ipv6(self, create_dict): + amock = self.set_mock('SoftLayer_Product_Package', 'getItems') + amock.return_value = fixtures.SoftLayer_Product_Package.getItems_1_IPV6_ADDRESS + create_dict.return_value = {'test': 1, 'verify': 1} + guest = {'test': 1, 'verify': 1, 'tags': ['First'], 'ipv6': True} + result = self.vs.order_guest(guest, test=True) + self.assertEqual(1234, result['orderId']) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate') + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=200) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + + @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') + def test_order_guest_placement_group(self, create_dict): + create_dict.return_value = {'test': 1, 'verify': 1} + guest = {'test': 1, 'verify': 1, 'placement_id': 5} + result = self.vs.order_guest(guest, test=True) + + call = self.calls('SoftLayer_Product_Order', 'verifyOrder')[0] + order_container = call.args[0] + + self.assertEqual(1234, result['orderId']) + self.assertEqual(5, order_container['virtualGuests'][0]['placementGroupId']) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate') + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + + def test_get_price_id_empty(self): + upgrade_prices = [ + {'categories': None, 'item': None}, + {'categories': [{'categoryCode': 'ram'}], 'item': None}, + {'categories': None, 'item': {'capacity': 1}}, + ] + result = self.vs._get_price_id_for_upgrade_option(upgrade_prices, 'memory', 1) + self.assertEqual(None, result) + + def test_get_price_id_memory_capacity(self): + upgrade_prices = [ + {'categories': [{'categoryCode': 'ram'}], 'item': {'capacity': 1}, 'id': 99} + ] + result = self.vs._get_price_id_for_upgrade_option(upgrade_prices, 'memory', 1) + self.assertEqual(99, result) + + def test_get_price_id_mismatch_capacity(self): + upgrade_prices = [ + {'categories': [{'categoryCode': 'ram1'}], 'item': {'capacity': 1}, 'id': 90}, + {'categories': [{'categoryCode': 'ram'}], 'item': {'capacity': 2}, 'id': 91}, + {'categories': [{'categoryCode': 'ram'}], 'item': {'capacity': 1}, 'id': 92}, + ] + result = self.vs._get_price_id_for_upgrade_option(upgrade_prices, 'memory', 1) + self.assertEqual(92, result) diff --git a/tests/managers/vs/vs_placement_tests.py b/tests/managers/vs/vs_placement_tests.py new file mode 100644 index 000000000..b492f69bf --- /dev/null +++ b/tests/managers/vs/vs_placement_tests.py @@ -0,0 +1,77 @@ +""" + SoftLayer.tests.managers.vs.vs_placement_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. + +""" +import mock + +from SoftLayer.managers.vs_placement import PlacementManager +from SoftLayer import testing + + +class VSPlacementManagerTests(testing.TestCase): + + def set_up(self): + self.manager = PlacementManager(self.client) + + def test_list(self): + self.manager.list() + self.assert_called_with('SoftLayer_Account', 'getPlacementGroups', mask=mock.ANY) + + def test_list_mask(self): + mask = "mask[id]" + self.manager.list(mask) + self.assert_called_with('SoftLayer_Account', 'getPlacementGroups', mask=mask) + + def test_create(self): + placement_object = { + 'backendRouter': 1234, + 'name': 'myName', + 'ruleId': 1 + } + self.manager.create(placement_object) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'createObject', args=(placement_object,)) + + def test_get_object(self): + self.manager.get_object(1234) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject', identifier=1234, mask=mock.ANY) + + def test_get_object_with_mask(self): + mask = "mask[id]" + self.manager.get_object(1234, mask) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject', identifier=1234, mask=mask) + + def test_delete(self): + self.manager.delete(1234) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'deleteObject', identifier=1234) + + def test_get_id_from_name(self): + self.manager._get_id_from_name('test') + _filter = { + 'placementGroups': { + 'name': {'operation': 'test'} + } + } + self.assert_called_with('SoftLayer_Account', 'getPlacementGroups', filter=_filter, mask="mask[id, name]") + + def test_get_rule_id_from_name(self): + result = self.manager.get_rule_id_from_name('SPREAD') + self.assertEqual(result[0], 1) + result = self.manager.get_rule_id_from_name('SpReAd') + self.assertEqual(result[0], 1) + + def test_get_rule_id_from_name_failure(self): + result = self.manager.get_rule_id_from_name('SPREAD1') + self.assertEqual(result, []) + + def test_router_search(self): + result = self.manager.get_backend_router_id_from_hostname('bcr01a.ams01') + self.assertEqual(result[0], 117917) + result = self.manager.get_backend_router_id_from_hostname('bcr01A.AMS01') + self.assertEqual(result[0], 117917) + + def test_router_search_failure(self): + result = self.manager.get_backend_router_id_from_hostname('1234.ams01') + self.assertEqual(result, []) diff --git a/tests/managers/vs_tests.py b/tests/managers/vs/vs_tests.py similarity index 70% rename from tests/managers/vs_tests.py rename to tests/managers/vs/vs_tests.py index 456b5cbc3..2816f8b06 100644 --- a/tests/managers/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -1,5 +1,5 @@ """ - SoftLayer.tests.managers.vs_tests + SoftLayer.tests.managers.vs.vs_tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :license: MIT, see LICENSE for more details. @@ -16,8 +16,7 @@ class VSTests(testing.TestCase): def set_up(self): - self.vs = SoftLayer.VSManager(self.client, - SoftLayer.OrderingManager(self.client)) + self.vs = SoftLayer.VSManager(self.client, SoftLayer.OrderingManager(self.client)) def test_list_instances(self): results = self.vs.list_instances(hourly=True, monthly=True) @@ -61,6 +60,7 @@ def test_list_instances_with_filters(self): nic_speed=100, public_ip='1.2.3.4', private_ip='4.3.2.1', + transient=False, ) _filter = { @@ -79,7 +79,8 @@ def test_list_instances_with_filters(self): 'hostname': {'operation': '_= hostname'}, 'networkComponents': {'maxSpeed': {'operation': 100}}, 'primaryIpAddress': {'operation': '_= 1.2.3.4'}, - 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'} + 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'}, + 'transientGuestFlag': {'operation': False}, } } self.assert_called_with('SoftLayer_Account', 'getVirtualGuests', @@ -156,17 +157,6 @@ def test_reload_instance_with_new_os(self): args=args, identifier=1) - @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') - def test_create_verify(self, create_dict): - create_dict.return_value = {'test': 1, 'verify': 1} - - self.vs.verify_create_instance(test=1, verify=1, tags=['test', 'tags']) - - create_dict.assert_called_once_with(test=1, verify=1) - self.assert_called_with('SoftLayer_Virtual_Guest', - 'generateOrderTemplate', - args=({'test': 1, 'verify': 1},)) - @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict') def test_create_instance(self, create_dict): create_dict.return_value = {'test': 1, 'verify': 1} @@ -489,6 +479,28 @@ def test_generate_private_vlan(self): self.assertEqual(data, assert_data) + def test_generate_sec_group(self): + data = self.vs._generate_create_dict( + cpus=1, + memory=1, + hostname='test', + domain='test.com', + os_code="OS", + public_security_groups=[1, 2, 3], + private_security_groups=[4, 5, 6] + ) + + pub_sec_binding = data['primaryNetworkComponent']['securityGroupBindings'] + prv_sec_binding = data['primaryBackendNetworkComponent']['securityGroupBindings'] + # Public + self.assertEqual(pub_sec_binding[0]['securityGroup']['id'], 1) + self.assertEqual(pub_sec_binding[1]['securityGroup']['id'], 2) + self.assertEqual(pub_sec_binding[2]['securityGroup']['id'], 3) + # Private + self.assertEqual(prv_sec_binding[0]['securityGroup']['id'], 4) + self.assertEqual(prv_sec_binding[1]['securityGroup']['id'], 5) + self.assertEqual(prv_sec_binding[2]['securityGroup']['id'], 6) + def test_create_network_components_vlan_subnet_private_vlan_subnet_public(self): data = self.vs._create_network_components( private_vlan=1, @@ -792,10 +804,10 @@ def test_edit_full(self): self.assertEqual(result, True) args = ({ - 'hostname': 'new-host', - 'domain': 'new.sftlyr.ws', - 'notes': 'random notes', - },) + 'hostname': 'new-host', + 'domain': 'new.sftlyr.ws', + 'notes': 'random notes', + },) self.assert_called_with('SoftLayer_Virtual_Guest', 'editObject', identifier=100, args=args) @@ -843,249 +855,51 @@ def test_capture_additional_disks(self): args=args, identifier=1) - def test_upgrade(self): - # test single upgrade - result = self.vs.upgrade(1, cpus=4, public=False) - - 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(order_container['prices'], [{'id': 1007}]) - self.assertEqual(order_container['virtualGuests'], [{'id': 1}]) - - def test_upgrade_blank(self): - # Now test a blank upgrade - result = self.vs.upgrade(1) - - self.assertEqual(result, False) - self.assertEqual(self.calls('SoftLayer_Product_Order', 'placeOrder'), - []) - - def test_upgrade_full(self): - # Testing all parameters Upgrade - result = self.vs.upgrade(1, - cpus=4, - memory=2, - nic_speed=1000, - public=True) + def test_usage_vs_cpu(self): + result = self.vs.get_summary_data_usage('100', + start_date='2019-3-4', + end_date='2019-4-2', + valid_type='CPU0', + summary_period=300) - 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.assertIn({'id': 1144}, order_container['prices']) - self.assertIn({'id': 1133}, order_container['prices']) - 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 - - # test single upgrade - result = self.vs.upgrade(1, cpus=4, public=False) - - 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(order_container['prices'], [{'id': 115566}]) - self.assertEqual(order_container['virtualGuests'], [{'id': 1}]) - - def test_get_item_id_for_upgrade(self): - item_id = 0 - package_items = self.client['Product_Package'].getItems(id=46) - for item in package_items: - if ((item['prices'][0]['categories'][0]['id'] == 3) - and (item.get('capacity') == '2')): - item_id = item['prices'][0]['id'] - break - self.assertEqual(1133, item_id) - - def test_get_package_items(self): - self.vs._get_package_items() - self.assert_called_with('SoftLayer_Product_Package', 'getItems') - - def test_get_price_id_for_upgrade(self): - package_items = self.vs._get_package_items() - - price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, - option='cpus', - value='4') - self.assertEqual(1144, price_id) - - def test_get_price_id_for_upgrade_skips_location_price(self): - package_items = self.vs._get_package_items() - - price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, - option='cpus', - value='55') - self.assertEqual(None, price_id) - - def test_get_price_id_for_upgrade_finds_nic_price(self): - package_items = self.vs._get_package_items() - - price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, - option='memory', - value='2') - self.assertEqual(1133, price_id) - - def test_get_price_id_for_upgrade_finds_memory_price(self): - package_items = self.vs._get_package_items() + expected = fixtures.SoftLayer_Metric_Tracking_Object.getSummaryData + self.assertEqual(result, expected) - price_id = self.vs._get_price_id_for_upgrade(package_items=package_items, - option='nic_speed', - value='1000') - self.assertEqual(1122, price_id) + args = ('2019-3-4', '2019-4-2', [{"keyName": "CPU0", "summaryType": "max"}], 300) + self.assert_called_with('SoftLayer_Metric_Tracking_Object', + 'getSummaryData', args=args, identifier=1000) -class VSWaitReadyGoTests(testing.TestCase): + def test_usage_vs_memory(self): + result = self.vs.get_summary_data_usage('100', + start_date='2019-3-4', + end_date='2019-4-2', + valid_type='MEMORY_USAGE', + summary_period=300) - def set_up(self): - self.client = mock.MagicMock() - self.vs = SoftLayer.VSManager(self.client) - self.guestObject = self.client['Virtual_Guest'].getObject - - @mock.patch('SoftLayer.managers.vs.VSManager.wait_for_ready') - def test_wait_interface(self, ready): - # verify interface to wait_for_ready is intact - self.vs.wait_for_transaction(1, 1) - ready.assert_called_once_with(1, 1, delay=10, pending=True) - - def test_active_not_provisioned(self): - # active transaction and no provision date should be false - self.guestObject.return_value = {'activeTransaction': {'id': 1}} - value = self.vs.wait_for_ready(1, 0) - self.assertFalse(value) - - def test_active_and_provisiondate(self): - # active transaction and provision date should be True - self.guestObject.side_effect = [ - {'activeTransaction': {'id': 1}, - 'provisionDate': 'aaa'}, - ] - value = self.vs.wait_for_ready(1, 1) - self.assertTrue(value) - - @mock.patch('time.sleep') - @mock.patch('time.time') - def test_active_provision_pending(self, _now, _sleep): - _now.side_effect = [0, 0, 1, 1, 2, 2] - # active transaction and provision date - # and pending should be false - self.guestObject.return_value = {'activeTransaction': {'id': 2}, 'provisionDate': 'aaa'} - - value = self.vs.wait_for_ready(instance_id=1, limit=1, delay=1, pending=True) - _sleep.assert_has_calls([mock.call(0)]) - self.assertFalse(value) - - def test_reload_no_pending(self): - # reload complete, maintance transactions - self.guestObject.return_value = { - 'activeTransaction': {'id': 2}, - 'provisionDate': 'aaa', - 'lastOperatingSystemReload': {'id': 1}, - } + expected = fixtures.SoftLayer_Metric_Tracking_Object.getSummaryData + self.assertEqual(result, expected) - value = self.vs.wait_for_ready(1, 1) - self.assertTrue(value) - - @mock.patch('time.sleep') - @mock.patch('time.time') - def test_reload_pending(self, _now, _sleep): - _now.side_effect = [0, 0, 1, 1, 2, 2] - # reload complete, pending maintance transactions - self.guestObject.return_value = {'activeTransaction': {'id': 2}, - 'provisionDate': 'aaa', - 'lastOperatingSystemReload': {'id': 1}} - value = self.vs.wait_for_ready(instance_id=1, limit=1, delay=1, pending=True) - _sleep.assert_has_calls([mock.call(0)]) - self.assertFalse(value) - - @mock.patch('time.sleep') - def test_ready_iter_once_incomplete(self, _sleep): - # no iteration, false - self.guestObject.return_value = {'activeTransaction': {'id': 1}} - value = self.vs.wait_for_ready(1, 0, delay=1) - self.assertFalse(value) - _sleep.assert_has_calls([mock.call(0)]) - - @mock.patch('time.sleep') - def test_iter_once_complete(self, _sleep): - # no iteration, true - self.guestObject.return_value = {'provisionDate': 'aaa'} - value = self.vs.wait_for_ready(1, 1, delay=1) - self.assertTrue(value) - self.assertFalse(_sleep.called) - - @mock.patch('time.sleep') - def test_iter_four_complete(self, _sleep): - # test 4 iterations with positive match - self.guestObject.side_effect = [ - {'activeTransaction': {'id': 1}}, - {'activeTransaction': {'id': 1}}, - {'activeTransaction': {'id': 1}}, - {'provisionDate': 'aaa'}, - ] - - value = self.vs.wait_for_ready(1, 4, delay=1) - self.assertTrue(value) - _sleep.assert_has_calls([mock.call(1), mock.call(1), mock.call(1)]) - self.guestObject.assert_has_calls([ - mock.call(id=1, mask=mock.ANY), mock.call(id=1, mask=mock.ANY), - mock.call(id=1, mask=mock.ANY), mock.call(id=1, mask=mock.ANY), - ]) - - @mock.patch('time.time') - @mock.patch('time.sleep') - def test_iter_two_incomplete(self, _sleep, _time): - # test 2 iterations, with no matches - self.guestObject.side_effect = [ - {'activeTransaction': {'id': 1}}, - {'activeTransaction': {'id': 1}}, - {'activeTransaction': {'id': 1}}, - {'provisionDate': 'aaa'} - ] - # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. - _time.side_effect = [0, 1, 2, 3, 4, 5, 6] - value = self.vs.wait_for_ready(1, 2, delay=1) - self.assertFalse(value) - _sleep.assert_has_calls([mock.call(1), mock.call(0)]) - self.guestObject.assert_has_calls([ - mock.call(id=1, mask=mock.ANY), - mock.call(id=1, mask=mock.ANY), - ]) - - @mock.patch('time.time') - @mock.patch('time.sleep') - def test_iter_20_incomplete(self, _sleep, _time): - """Wait for up to 20 seconds (sleeping for 10 seconds) for a server.""" - self.guestObject.return_value = {'activeTransaction': {'id': 1}} - # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. - _time.side_effect = [0, 0, 10, 10, 20, 20, 50, 60] - value = self.vs.wait_for_ready(1, 20, delay=10) - self.assertFalse(value) - self.guestObject.assert_has_calls([mock.call(id=1, mask=mock.ANY)]) - - _sleep.assert_has_calls([mock.call(10)]) - - @mock.patch('SoftLayer.decoration.sleep') - @mock.patch('SoftLayer.transports.FixtureTransport.__call__') - @mock.patch('time.time') - @mock.patch('time.sleep') - def test_exception_from_api(self, _sleep, _time, _vs, _dsleep): - """Tests escalating scale back when an excaption is thrown""" - _dsleep.return_value = False - - self.guestObject.side_effect = [ - exceptions.TransportError(104, "Its broken"), - {'activeTransaction': {'id': 1}}, - {'provisionDate': 'aaa'} - ] - # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. - _time.side_effect = [0, 1, 2, 3, 4] - value = self.vs.wait_for_ready(1, 20, delay=1) - _sleep.assert_called_once() - _dsleep.assert_called_once() - self.assertTrue(value) + args = ('2019-3-4', '2019-4-2', [{"keyName": "MEMORY_USAGE", "summaryType": "max"}], 300) + + self.assert_called_with('SoftLayer_Metric_Tracking_Object', 'getSummaryData', args=args, identifier=1000) + + def test_get_tracking_id(self): + result = self.vs.get_tracking_id(1234) + self.assert_called_with('SoftLayer_Virtual_Guest', 'getMetricTrackingObjectId') + self.assertEqual(result, 1000) + + def test_get_bandwidth_data(self): + result = self.vs.get_bandwidth_data(1234, '2019-01-01', '2019-02-01', 'public', 1000) + self.assert_called_with('SoftLayer_Metric_Tracking_Object', + 'getBandwidthData', + args=('2019-01-01', '2019-02-01', 'public', 1000), + identifier=1000) + self.assertEqual(result[0]['type'], 'cpu0') + + def test_get_bandwidth_allocation(self): + result = self.vs.get_bandwidth_allocation(1234) + self.assert_called_with('SoftLayer_Virtual_Guest', 'getBandwidthAllotmentDetail', identifier=1234) + self.assert_called_with('SoftLayer_Virtual_Guest', 'getBillingCycleBandwidthUsage', identifier=1234) + self.assertEqual(result['allotment']['amount'], '250') + self.assertEqual(result['useage'][0]['amountIn'], '.448') diff --git a/tests/managers/vs/vs_waiting_for_ready_tests.py b/tests/managers/vs/vs_waiting_for_ready_tests.py new file mode 100644 index 000000000..4308bd55d --- /dev/null +++ b/tests/managers/vs/vs_waiting_for_ready_tests.py @@ -0,0 +1,163 @@ +""" + SoftLayer.tests.managers.vs.vs_waiting_for_ready_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. + +""" +import mock + +import SoftLayer +from SoftLayer import exceptions +from SoftLayer import testing + + +class VSWaitReadyGoTests(testing.TestCase): + + def set_up(self): + self.client = mock.MagicMock() + self.vs = SoftLayer.VSManager(self.client) + self.guestObject = self.client['Virtual_Guest'].getObject + + @mock.patch('SoftLayer.managers.vs.VSManager.wait_for_ready') + def test_wait_interface(self, ready): + # verify interface to wait_for_ready is intact + self.vs.wait_for_transaction(1, 1) + ready.assert_called_once_with(1, 1, delay=10, pending=True) + + def test_active_not_provisioned(self): + # active transaction and no provision date should be false + self.guestObject.return_value = {'activeTransaction': {'id': 1}} + value = self.vs.wait_for_ready(1, 0) + self.assertFalse(value) + + def test_active_and_provisiondate(self): + # active transaction and provision date should be True + self.guestObject.side_effect = [ + {'activeTransaction': {'id': 1}, + 'provisionDate': 'aaa'}, + ] + value = self.vs.wait_for_ready(1, 1) + self.assertTrue(value) + + @mock.patch('time.sleep') + @mock.patch('time.time') + def test_active_provision_pending(self, _now, _sleep): + _now.side_effect = [0, 0, 1, 1, 2, 2] + # active transaction and provision date + # and pending should be false + self.guestObject.return_value = {'activeTransaction': {'id': 2}, 'provisionDate': 'aaa'} + + value = self.vs.wait_for_ready(instance_id=1, limit=1, delay=1, pending=True) + _sleep.assert_has_calls([mock.call(0)]) + self.assertFalse(value) + + def test_reload_no_pending(self): + # reload complete, maintance transactions + self.guestObject.return_value = { + 'activeTransaction': {'id': 2}, + 'provisionDate': 'aaa', + 'lastOperatingSystemReload': {'id': 1}, + } + + value = self.vs.wait_for_ready(1, 1) + self.assertTrue(value) + + @mock.patch('time.sleep') + @mock.patch('time.time') + def test_reload_pending(self, _now, _sleep): + _now.side_effect = [0, 0, 1, 1, 2, 2] + # reload complete, pending maintance transactions + self.guestObject.return_value = {'activeTransaction': {'id': 2}, + 'provisionDate': 'aaa', + 'lastOperatingSystemReload': {'id': 1}} + value = self.vs.wait_for_ready(instance_id=1, limit=1, delay=1, pending=True) + _sleep.assert_has_calls([mock.call(0)]) + self.assertFalse(value) + + @mock.patch('time.sleep') + def test_ready_iter_once_incomplete(self, _sleep): + # no iteration, false + self.guestObject.return_value = {'activeTransaction': {'id': 1}} + value = self.vs.wait_for_ready(1, 0, delay=1) + self.assertFalse(value) + _sleep.assert_has_calls([mock.call(0)]) + + @mock.patch('time.sleep') + def test_iter_once_complete(self, _sleep): + # no iteration, true + self.guestObject.return_value = {'provisionDate': 'aaa'} + value = self.vs.wait_for_ready(1, 1, delay=1) + self.assertTrue(value) + self.assertFalse(_sleep.called) + + @mock.patch('time.sleep') + def test_iter_four_complete(self, _sleep): + # test 4 iterations with positive match + self.guestObject.side_effect = [ + {'activeTransaction': {'id': 1}}, + {'activeTransaction': {'id': 1}}, + {'activeTransaction': {'id': 1}}, + {'provisionDate': 'aaa'}, + ] + + value = self.vs.wait_for_ready(1, 4, delay=1) + self.assertTrue(value) + _sleep.assert_has_calls([mock.call(1), mock.call(1), mock.call(1)]) + self.guestObject.assert_has_calls([ + mock.call(id=1, mask=mock.ANY), mock.call(id=1, mask=mock.ANY), + mock.call(id=1, mask=mock.ANY), mock.call(id=1, mask=mock.ANY), + ]) + + @mock.patch('time.time') + @mock.patch('time.sleep') + def test_iter_two_incomplete(self, _sleep, _time): + # test 2 iterations, with no matches + self.guestObject.side_effect = [ + {'activeTransaction': {'id': 1}}, + {'activeTransaction': {'id': 1}}, + {'activeTransaction': {'id': 1}}, + {'provisionDate': 'aaa'} + ] + # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. + _time.side_effect = [0, 1, 2, 3, 4, 5, 6] + value = self.vs.wait_for_ready(1, 2, delay=1) + self.assertFalse(value) + _sleep.assert_has_calls([mock.call(1), mock.call(0)]) + self.guestObject.assert_has_calls([ + mock.call(id=1, mask=mock.ANY), + mock.call(id=1, mask=mock.ANY), + ]) + + @mock.patch('time.time') + @mock.patch('time.sleep') + def test_iter_20_incomplete(self, _sleep, _time): + """Wait for up to 20 seconds (sleeping for 10 seconds) for a server.""" + self.guestObject.return_value = {'activeTransaction': {'id': 1}} + # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. + _time.side_effect = [0, 0, 10, 10, 20, 20, 50, 60] + value = self.vs.wait_for_ready(1, 20, delay=10) + self.assertFalse(value) + self.guestObject.assert_has_calls([mock.call(id=1, mask=mock.ANY)]) + + _sleep.assert_has_calls([mock.call(10)]) + + @mock.patch('SoftLayer.decoration.sleep') + @mock.patch('SoftLayer.transports.FixtureTransport.__call__') + @mock.patch('time.time') + @mock.patch('time.sleep') + def test_exception_from_api(self, _sleep, _time, _vs, _dsleep): + """Tests escalating scale back when an excaption is thrown""" + _dsleep.return_value = False + + self.guestObject.side_effect = [ + exceptions.ServerError(504, "Its broken"), + {'activeTransaction': {'id': 1}}, + {'provisionDate': 'aaa'} + ] + # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. + _time.side_effect = [0, 1, 2, 3, 4] + value = self.vs.wait_for_ready(1, 20, delay=1) + _sleep.assert_called_once() + _dsleep.assert_called_once() + self.assertTrue(value) diff --git a/tests/transport_tests.py b/tests/transport_tests.py index 87a43de62..e7a71a6fa 100644 --- a/tests/transport_tests.py +++ b/tests/transport_tests.py @@ -78,7 +78,8 @@ def test_call(self, request): data=data, timeout=None, cert=None, - verify=True) + verify=True, + auth=None) self.assertEqual(resp, []) self.assertIsInstance(resp, transports.SoftLayerListResult) self.assertEqual(resp.total_count, 10) @@ -114,7 +115,8 @@ def test_valid_proxy(self, request): headers=mock.ANY, timeout=None, cert=None, - verify=True) + verify=True, + auth=None) @mock.patch('SoftLayer.transports.requests.Session.request') def test_identifier(self, request): @@ -264,6 +266,50 @@ def test_print_reproduceable(self): output_text = self.transport.print_reproduceable(req) self.assertIn("https://test.com", output_text) + @mock.patch('SoftLayer.transports.requests.Session.request') + @mock.patch('requests.auth.HTTPBasicAuth') + def test_ibm_id_call(self, auth, request): + request.return_value = self.response + + data = ''' + +getObject + + + + +headers + + + + + + + +''' + + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'getObject' + req.transport_user = 'apikey' + req.transport_password = '1234567890qweasdzxc' + resp = self.transport(req) + + auth.assert_called_with('apikey', '1234567890qweasdzxc') + request.assert_called_with('POST', + 'http://something.com/SoftLayer_Service', + headers={'Content-Type': 'application/xml', + 'User-Agent': consts.USER_AGENT}, + proxies=None, + data=data, + timeout=None, + cert=None, + verify=True, + auth=mock.ANY) + self.assertEqual(resp, []) + self.assertIsInstance(resp, transports.SoftLayerListResult) + self.assertEqual(resp.total_count, 10) + @mock.patch('SoftLayer.transports.requests.Session.request') @pytest.mark.parametrize( @@ -311,7 +357,8 @@ def test_verify(request, cert=mock.ANY, proxies=mock.ANY, timeout=mock.ANY, - verify=expected) + verify=expected, + auth=None) class TestRestAPICall(testing.TestCase): @@ -349,15 +396,29 @@ def test_basic(self, request): timeout=None) @mock.patch('SoftLayer.transports.requests.Session.request') - def test_error(self, request): + def test_http_and_json_error(self, request): # Test JSON Error e = requests.HTTPError('error') e.response = mock.MagicMock() e.response.status_code = 404 - e.response.text = '''{ + e.response.text = ''' "error": "description", "code": "Error Code" - }''' + ''' + request().raise_for_status.side_effect = e + + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'Resource' + self.assertRaises(SoftLayer.SoftLayerAPIError, self.transport, req) + + @mock.patch('SoftLayer.transports.requests.Session.request') + def test_http_and_empty_error(self, request): + # Test JSON Error + e = requests.HTTPError('error') + e.response = mock.MagicMock() + e.response.status_code = 404 + e.response.text = '' request().raise_for_status.side_effect = e req = transports.Request() @@ -365,6 +426,26 @@ def test_error(self, request): req.method = 'Resource' self.assertRaises(SoftLayer.SoftLayerAPIError, self.transport, req) + @mock.patch('SoftLayer.transports.requests.Session.request') + def test_empty_error(self, request): + # Test empty response error. + request().text = '' + + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'Resource' + self.assertRaises(SoftLayer.SoftLayerAPIError, self.transport, req) + + @mock.patch('SoftLayer.transports.requests.Session.request') + def test_json_error(self, request): + # Test non-json response error. + request().text = 'Not JSON' + + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'Resource' + self.assertRaises(SoftLayer.SoftLayerAPIError, self.transport, req) + def test_proxy_without_protocol(self): req = transports.Request() req.service = 'SoftLayer_Service' diff --git a/tools/requirements.txt b/tools/requirements.txt index bed36edb5..880810646 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,6 +1,8 @@ -requests >= 2.18.4 -click >= 5, < 7 prettytable >= 0.7.0 six >= 1.7.0 -prompt_toolkit -urllib3 +ptable >= 0.9.2 +click >= 7 +requests >= 2.20.0 +prompt_toolkit >= 2 +pygments >= 2.0.0 +urllib3 >= 1.24 \ No newline at end of file diff --git a/tools/test-requirements.txt b/tools/test-requirements.txt index c9a94de27..2869de5e6 100644 --- a/tools/test-requirements.txt +++ b/tools/test-requirements.txt @@ -4,5 +4,10 @@ pytest-cov mock sphinx testtools -urllib3 -requests >= 2.18.4 +six >= 1.7.0 +ptable >= 0.9.2 +click >= 7 +requests >= 2.20.0 +prompt_toolkit >= 2 +pygments >= 2.0.0 +urllib3 >= 1.24 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 25e736cb8..ff08bac17 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py27,py35,py36,pypy,analysis,coverage +envlist = py35,py36,py37,pypy,analysis,coverage + [flake8] max-line-length=120 @@ -35,6 +36,10 @@ commands = -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 \