diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f6fd444a..0def27891 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,5 +25,15 @@ 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. + + 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 8a274c8e2..177f15143 100644 --- a/README.rst +++ b/README.rst @@ -88,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 @@ -118,6 +120,8 @@ 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, 3.6, or 3.7. 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/routes.py b/SoftLayer/CLI/routes.py index cebf2bc0b..51914c30b 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -31,6 +31,7 @@ ('virtual:upgrade', 'SoftLayer.CLI.virt.upgrade: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'), @@ -317,4 +318,5 @@ 'vm': 'virtual', 'vs': 'virtual', 'dh': 'dedicatedhost', + 'pg': 'placementgroup', } diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index 3ee44b3a0..631793475 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -32,6 +32,7 @@ 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), } like_args['flavor'] = utils.lookup(like_details, @@ -138,6 +139,10 @@ def _parse_create_args(client, args): 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') + return data @@ -190,6 +195,8 @@ def _parse_create_args(client, args): 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") @environment.pass_env def cli(env, **args): diff --git a/SoftLayer/CLI/virt/placementgroup/__init__.py b/SoftLayer/CLI/virt/placementgroup/__init__.py new file mode 100644 index 000000000..02d5da986 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/__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 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""" + pass diff --git a/SoftLayer/CLI/virt/placementgroup/create.py b/SoftLayer/CLI/virt/placementgroup/create.py new file mode 100644 index 000000000..af1fb8db5 --- /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..3107fc334 --- /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 Reserved Capacity""" + 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..365205e74 --- /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 Reserved Capacity 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/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 891df9ecb..b4bafac92 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -591,7 +591,7 @@ 'modifyDate': '', 'name': 'test-capacity', 'availableInstanceCount': 1, - 'instanceCount': 2, + 'instanceCount': 3, 'occupiedInstanceCount': 1, 'backendRouter': { 'accountId': 1, @@ -638,7 +638,27 @@ '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" + } +}] 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/managers/__init__.py b/SoftLayer/managers/__init__.py index b6cc1faa5..02e54b30e 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -28,7 +28,7 @@ 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', @@ -47,6 +47,7 @@ 'NetworkManager', 'ObjectStorageManager', 'OrderingManager', + 'PlacementManager', 'SshKeyManager', 'SSLManager', 'TicketManager', diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 785ad3834..0f5a6d26a 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -233,7 +233,8 @@ def get_instance(self, instance_id, **kwargs): preset.keyName]],''' 'tagReferences[id,tag[name,id]],' 'networkVlans[id,vlanNumber,networkSpace],' - 'dedicatedHost.id' + 'dedicatedHost.id,' + 'placementGroupId' ) return self.guest.getObject(id=instance_id, **kwargs) @@ -875,6 +876,7 @@ def order_guest(self, guest_object, test=False): :param dictionary guest_object: See SoftLayer.CLI.virt.create._parse_create_args Example:: + new_vsi = { 'domain': u'test01.labs.sftlyr.ws', 'hostname': u'minion05', @@ -909,6 +911,8 @@ def order_guest(self, guest_object, test=False): # 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('placement_id'): + template['virtualGuests'][0]['placementGroupId'] = guest_object.get('placement_id') if test: result = self.client.call('Product_Order', 'verifyOrder', template) else: diff --git a/SoftLayer/managers/vs_placement.py b/SoftLayer/managers/vs_placement.py new file mode 100644 index 000000000..d40a845e9 --- /dev/null +++ b/SoftLayer/managers/vs_placement.py @@ -0,0 +1,115 @@ +""" + 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 + + :param dictionary placement_object: Below are the fields you can specify, taken from + https://softlayer.github.io/reference/datatypes/SoftLayer_Virtual_PlacementGroup/ + placement_object = { + 'backendRouterId': 12345, + 'name': 'Test Name', + 'ruleId': 12345 + } + + """ + 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/transports.py b/SoftLayer/transports.py index 3aa896f11..9ff8bdaf3 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', 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 55ee3c189..2276bd7e9 100644 --- a/docs/cli/vs.rst +++ b/docs/cli/vs.rst @@ -81,15 +81,33 @@ datacenter using the command `slcli vs create`. :: - $ slcli vs create --hostname=example --domain=softlayer.com --cpu 2 --memory 1024 -o DEBIAN_LATEST_64 --datacenter=ams01 --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 @@ -194,3 +212,10 @@ Reserved Capacity vs/reserved_capacity +Placement Groups +---------------- +.. toctree:: + :maxdepth: 2 + + vs/placement_group + 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/tests/CLI/modules/vs/vs_capacity_tests.py b/tests/CLI/modules/vs/vs_capacity_tests.py index 3dafee347..2cee000a0 100644 --- a/tests/CLI/modules/vs/vs_capacity_tests.py +++ b/tests/CLI/modules/vs/vs_capacity_tests.py @@ -4,6 +4,7 @@ :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 @@ -15,6 +16,26 @@ 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) 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/managers/vs/vs_capacity_tests.py b/tests/managers/vs/vs_capacity_tests.py index 751b31753..5229ebec4 100644 --- a/tests/managers/vs/vs_capacity_tests.py +++ b/tests/managers/vs/vs_capacity_tests.py @@ -46,6 +46,16 @@ def test_get_available_routers(self): 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 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, [])