From 7f6f0d2f51369a80a432d838ff3a429edf8ce782 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 18 Jan 2019 16:13:09 -0600 Subject: [PATCH 1/8] #1069 basic support for virtual placement groups --- SoftLayer/CLI/routes.py | 2 + SoftLayer/CLI/virt/create.py | 4 + SoftLayer/CLI/virt/placementgroup/__init__.py | 47 +++++++++ SoftLayer/CLI/virt/placementgroup/create.py | 49 ++++++++++ SoftLayer/CLI/virt/placementgroup/delete.py | 53 ++++++++++ SoftLayer/CLI/virt/placementgroup/detail.py | 55 +++++++++++ SoftLayer/CLI/virt/placementgroup/list.py | 32 +++++++ SoftLayer/managers/vs.py | 5 +- SoftLayer/managers/vs_placement.py | 96 +++++++++++++++++++ SoftLayer/transports.py | 2 +- 10 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 SoftLayer/CLI/virt/placementgroup/__init__.py create mode 100644 SoftLayer/CLI/virt/placementgroup/create.py create mode 100644 SoftLayer/CLI/virt/placementgroup/delete.py create mode 100644 SoftLayer/CLI/virt/placementgroup/detail.py create mode 100644 SoftLayer/CLI/virt/placementgroup/list.py create mode 100644 SoftLayer/managers/vs_placement.py 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..c9639db29 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['placementGroupId'] or None, } like_args['flavor'] = utils.lookup(like_details, @@ -90,6 +91,7 @@ def _parse_create_args(client, args): "datacenter": args.get('datacenter', None), "public_vlan": args.get('vlan_public', None), "private_vlan": args.get('vlan_private', None), + "placement_id": args.get('placement_id', None), "public_subnet": args.get('subnet_public', None), "private_subnet": args.get('subnet_private', None), } @@ -190,6 +192,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('--placement-id', type=click.INT, + help="Placement Group 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..65ba3ea5a --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/create.py @@ -0,0 +1,49 @@ +"""Create a placement group""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + +from pprint import pprint as pp + +def _get_routers(ctx, _, value): + if not value or ctx.resilient_parsing: + return + env = ctx.ensure_object(environment.Environment) + manager = PlacementManager(env.client) + routers = manager.get_routers() + env.fout(get_router_table(routers)) + ctx.exit() + +@click.command() +@click.option('--name', type=click.STRING, required=True, prompt=True, help="Name for this new placement group.") +@click.option('--backend_router_id', '-b', type=click.INT, required=True, prompt=True, + help="backendRouterId, use --list_routers/-l to print out a list of available ids.") +@click.option('--list_routers', '-l', is_flag=True, callback=_get_routers, is_eager=True, + help="Prints available backend router ids and exit.") +@environment.pass_env +def cli(env, **args): + """Create a placement group""" + manager = PlacementManager(env.client) + placement_object = { + 'name': args.get('name'), + 'backendRouterId': args.get('backend_router_id'), + 'ruleId': 1 # Hard coded as there is only 1 rule at the moment + } + + result = manager.create(placement_object) + click.secho("Successfully created placement group: ID: %s, Name: %s" % (result['id'], result['name']), fg='green') + + +def get_router_table(routers): + 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 + + + + diff --git a/SoftLayer/CLI/virt/placementgroup/delete.py b/SoftLayer/CLI/virt/placementgroup/delete.py new file mode 100644 index 000000000..ca6203d61 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/delete.py @@ -0,0 +1,53 @@ +"""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_placement import PlacementManager as PlacementManager +from SoftLayer.managers.vs import VSManager as VSManager + +from pprint import pprint as pp + + +@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.") +@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: + # pass + 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 True + + 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') + + + # pp(result) \ No newline at end of file diff --git a/SoftLayer/CLI/virt/placementgroup/detail.py b/SoftLayer/CLI/virt/placementgroup/detail.py new file mode 100644 index 000000000..464db4575 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/detail.py @@ -0,0 +1,55 @@ +"""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 + +from pprint import pprint as pp + +@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..2536b00f5 --- /dev/null +++ b/SoftLayer/CLI/virt/placementgroup/list.py @@ -0,0 +1,32 @@ +"""List Placement Groups""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + +from pprint import pprint as pp + +@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) + # pp(result) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 785ad3834..2212460a7 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) @@ -909,6 +910,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..f94010051 --- /dev/null +++ b/SoftLayer/managers/vs_placement.py @@ -0,0 +1,96 @@ +""" + SoftLayer.vs_placement + ~~~~~~~~~~~~~~~~~~~~~~~ + Placement Group Manager + + :license: MIT, see License for more details. +""" + +import logging +import SoftLayer + +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): + 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_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', From aa28ab822c8a9f3eac557ae9db97d224c36cfa73 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 18 Jan 2019 17:09:53 -0600 Subject: [PATCH 2/8] unit tests for the cli portion of placement groups --- SoftLayer/CLI/virt/placementgroup/create.py | 1 - SoftLayer/CLI/virt/placementgroup/delete.py | 11 +-- SoftLayer/CLI/virt/placementgroup/detail.py | 1 - SoftLayer/CLI/virt/placementgroup/list.py | 3 - SoftLayer/fixtures/SoftLayer_Account.py | 17 ++++ .../SoftLayer_Virtual_PlacementGroup.py | 63 ++++++++++++ tests/CLI/modules/vs/vs_placement_tests.py | 99 +++++++++++++++++++ 7 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup.py create mode 100644 tests/CLI/modules/vs/vs_placement_tests.py diff --git a/SoftLayer/CLI/virt/placementgroup/create.py b/SoftLayer/CLI/virt/placementgroup/create.py index 65ba3ea5a..ca5be94ba 100644 --- a/SoftLayer/CLI/virt/placementgroup/create.py +++ b/SoftLayer/CLI/virt/placementgroup/create.py @@ -6,7 +6,6 @@ from SoftLayer.CLI import formatting from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager -from pprint import pprint as pp def _get_routers(ctx, _, value): if not value or ctx.resilient_parsing: diff --git a/SoftLayer/CLI/virt/placementgroup/delete.py b/SoftLayer/CLI/virt/placementgroup/delete.py index ca6203d61..717a157ee 100644 --- a/SoftLayer/CLI/virt/placementgroup/delete.py +++ b/SoftLayer/CLI/virt/placementgroup/delete.py @@ -9,17 +9,18 @@ from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager from SoftLayer.managers.vs import VSManager as VSManager -from pprint import pprint as pp - @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.") +@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) @@ -27,7 +28,6 @@ def cli(env, identifier, purge): if purge: - # pass placement_group = manager.get_object(group_id) guest_list = ', '.join([guest['fullyQualifiedDomainName'] for guest in placement_group['guests']]) if len(placement_group['guests']) < 1: @@ -48,6 +48,3 @@ def cli(env, identifier, purge): cancel_result = manager.delete(group_id) if cancel_result: click.secho("Placement Group %s has been canceld." % identifier, fg='green') - - - # pp(result) \ No newline at end of file diff --git a/SoftLayer/CLI/virt/placementgroup/detail.py b/SoftLayer/CLI/virt/placementgroup/detail.py index 464db4575..9adf58932 100644 --- a/SoftLayer/CLI/virt/placementgroup/detail.py +++ b/SoftLayer/CLI/virt/placementgroup/detail.py @@ -7,7 +7,6 @@ from SoftLayer.CLI import helpers from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager -from pprint import pprint as pp @click.command(epilog="Once provisioned, virtual guests can be managed with the slcli vs commands") @click.argument('identifier') diff --git a/SoftLayer/CLI/virt/placementgroup/list.py b/SoftLayer/CLI/virt/placementgroup/list.py index 2536b00f5..b2ae7eb32 100644 --- a/SoftLayer/CLI/virt/placementgroup/list.py +++ b/SoftLayer/CLI/virt/placementgroup/list.py @@ -6,8 +6,6 @@ from SoftLayer.CLI import formatting from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager -from pprint import pprint as pp - @click.command() @environment.pass_env def cli(env): @@ -29,4 +27,3 @@ def cli(env): ]) env.fout(table) - # pp(result) diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 891df9ecb..032b06fd8 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -642,3 +642,20 @@ ] } ] + + +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" + } +}] \ No newline at end of file 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/tests/CLI/modules/vs/vs_placement_tests.py b/tests/CLI/modules/vs/vs_placement_tests.py new file mode 100644 index 000000000..119ec4ac3 --- /dev/null +++ b/tests/CLI/modules/vs/vs_placement_tests.py @@ -0,0 +1,99 @@ +""" + SoftLayer.tests.CLI.modules.vs_placement_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +import json + +import mock + +from SoftLayer.CLI import exceptions +from SoftLayer import SoftLayerAPIError +from SoftLayer import testing + + + +class VSPlacementTests(testing.TestCase): + + def test_create_group_list_routers(self): + result = self.run_command(['vs', 'placementgroup', 'create', '--list_routers']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getAvailableRouters') + self.assertEquals([], 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_id=1']) + create_args = { + 'name': 'test', + 'backendRouterId': 1, + 'ruleId': 1 + } + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'createObject', args=(create_args,)) + self.assertEquals([], 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_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_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.assertEquals(result.exit_code, 2) + self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject') + self.assertEquals([], self.calls('SoftLayer_Virtual_Guest', 'deleteObject')) From 3d03455517c53fface4db0b6cb01de429e2b7c67 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Mon, 21 Jan 2019 17:23:13 -0600 Subject: [PATCH 3/8] #1069 unit tests for the placement manageR --- tests/managers/vs/vs_placement_tests.py | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/managers/vs/vs_placement_tests.py diff --git a/tests/managers/vs/vs_placement_tests.py b/tests/managers/vs/vs_placement_tests.py new file mode 100644 index 000000000..55bbdd987 --- /dev/null +++ b/tests/managers/vs/vs_placement_tests.py @@ -0,0 +1,61 @@ +""" + SoftLayer.tests.managers.vs.vs_placement_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 +from SoftLayer.managers.vs_placement import PlacementManager + + +class VSPlacementManagerTests(testing.TestCase): + + def set_up(self): + self.manager = PlacementManager(self.client) + amock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + + 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): + result = 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]") \ No newline at end of file From e3ed32ba51c384107d58aaf8ef79c05482192fa9 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 22 Jan 2019 15:28:53 -0600 Subject: [PATCH 4/8] unit test fixes --- SoftLayer/CLI/virt/create.py | 2 +- SoftLayer/managers/vs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index c9639db29..c765831ee 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -32,7 +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['placementGroupId'] or None, + 'placement_id': like_details.get('placementGroupId', None), } like_args['flavor'] = utils.lookup(like_details, diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 2212460a7..405af146e 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -233,7 +233,7 @@ def get_instance(self, instance_id, **kwargs): preset.keyName]],''' 'tagReferences[id,tag[name,id]],' 'networkVlans[id,vlanNumber,networkSpace],' - 'dedicatedHost.id', + 'dedicatedHost.id,' 'placementGroupId' ) From ab5a167f31834b70c321c80b6e7a8adb3931b144 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 22 Jan 2019 16:59:13 -0600 Subject: [PATCH 5/8] style fixes --- SoftLayer/CLI/virt/placementgroup/create.py | 9 ++++----- SoftLayer/CLI/virt/placementgroup/delete.py | 9 ++++----- SoftLayer/CLI/virt/placementgroup/list.py | 1 + SoftLayer/fixtures/SoftLayer_Account.py | 2 +- SoftLayer/managers/vs.py | 2 +- SoftLayer/managers/vs_placement.py | 15 +++++++-------- tests/CLI/modules/vs/vs_placement_tests.py | 21 ++++++++------------- tests/managers/vs/vs_placement_tests.py | 12 ++++-------- 8 files changed, 30 insertions(+), 41 deletions(-) diff --git a/SoftLayer/CLI/virt/placementgroup/create.py b/SoftLayer/CLI/virt/placementgroup/create.py index ca5be94ba..a6ee49606 100644 --- a/SoftLayer/CLI/virt/placementgroup/create.py +++ b/SoftLayer/CLI/virt/placementgroup/create.py @@ -8,6 +8,7 @@ def _get_routers(ctx, _, value): + """Prints out the available routers that can be used for placement groups """ if not value or ctx.resilient_parsing: return env = ctx.ensure_object(environment.Environment) @@ -16,6 +17,7 @@ def _get_routers(ctx, _, value): env.fout(get_router_table(routers)) ctx.exit() + @click.command() @click.option('--name', type=click.STRING, required=True, prompt=True, help="Name for this new placement group.") @click.option('--backend_router_id', '-b', type=click.INT, required=True, prompt=True, @@ -29,7 +31,7 @@ def cli(env, **args): placement_object = { 'name': args.get('name'), 'backendRouterId': args.get('backend_router_id'), - 'ruleId': 1 # Hard coded as there is only 1 rule at the moment + 'ruleId': 1 # Hard coded as there is only 1 rule at the moment } result = manager.create(placement_object) @@ -37,12 +39,9 @@ def cli(env, **args): 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 - - - - diff --git a/SoftLayer/CLI/virt/placementgroup/delete.py b/SoftLayer/CLI/virt/placementgroup/delete.py index 717a157ee..260b3cd35 100644 --- a/SoftLayer/CLI/virt/placementgroup/delete.py +++ b/SoftLayer/CLI/virt/placementgroup/delete.py @@ -6,14 +6,14 @@ from SoftLayer.CLI import exceptions from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers -from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager 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. " \ +@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): @@ -26,7 +26,6 @@ def cli(env, identifier, purge): 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']]) @@ -40,7 +39,7 @@ def cli(env, identifier, purge): for guest in placement_group['guests']: click.secho("Deleting %s..." % guest['fullyQualifiedDomainName']) vm_manager.cancel_instance(guest['id']) - return True + 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?")): diff --git a/SoftLayer/CLI/virt/placementgroup/list.py b/SoftLayer/CLI/virt/placementgroup/list.py index b2ae7eb32..365205e74 100644 --- a/SoftLayer/CLI/virt/placementgroup/list.py +++ b/SoftLayer/CLI/virt/placementgroup/list.py @@ -6,6 +6,7 @@ from SoftLayer.CLI import formatting from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager + @click.command() @environment.pass_env def cli(env): diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 032b06fd8..b01be4083 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -658,4 +658,4 @@ "keyName": "SPREAD", "name": "SPREAD" } -}] \ No newline at end of file +}] diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 405af146e..644f80f00 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -911,7 +911,7 @@ def order_guest(self, guest_object, test=False): template['virtualGuests'][0]['userData'] = [{"value": guest_object.get('userdata')}] if guest_object.get('placement_id'): - template['virtualGuests'][0]['placementGroupId'] = 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 index f94010051..acd78c6d9 100644 --- a/SoftLayer/managers/vs_placement.py +++ b/SoftLayer/managers/vs_placement.py @@ -7,7 +7,6 @@ """ import logging -import SoftLayer from SoftLayer import utils @@ -39,6 +38,10 @@ def __init__(self, client): 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) @@ -54,7 +57,7 @@ def create(self, placement_object): 'name': 'Test Name', 'ruleId': 12345 } - + """ return self.client.call('SoftLayer_Virtual_PlacementGroup', 'createObject', placement_object) @@ -64,7 +67,7 @@ def get_routers(self): 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: @@ -72,7 +75,6 @@ def get_object(self, group_id, mask=None): "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 @@ -84,13 +86,10 @@ def delete(self, group_id): def _get_id_from_name(self, name): """List placement group ids which match the given name.""" _filter = { - 'placementGroups' : { + '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/tests/CLI/modules/vs/vs_placement_tests.py b/tests/CLI/modules/vs/vs_placement_tests.py index 119ec4ac3..ecff4f58f 100644 --- a/tests/CLI/modules/vs/vs_placement_tests.py +++ b/tests/CLI/modules/vs/vs_placement_tests.py @@ -4,23 +4,18 @@ :license: MIT, see LICENSE for more details. """ -import json - import mock -from SoftLayer.CLI import exceptions -from SoftLayer import SoftLayerAPIError from SoftLayer import testing - class VSPlacementTests(testing.TestCase): def test_create_group_list_routers(self): result = self.run_command(['vs', 'placementgroup', 'create', '--list_routers']) self.assert_no_fail(result) self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getAvailableRouters') - self.assertEquals([], self.calls('SoftLayer_Virtual_PlacementGroup', 'createObject')) + self.assertEqual([], self.calls('SoftLayer_Virtual_PlacementGroup', 'createObject')) @mock.patch('SoftLayer.CLI.formatting.confirm') def test_create_group(self, confirm_mock): @@ -33,7 +28,7 @@ def test_create_group(self, confirm_mock): } self.assert_no_fail(result) self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'createObject', args=(create_args,)) - self.assertEquals([], self.calls('SoftLayer_Virtual_PlacementGroup', 'getAvailableRouters')) + self.assertEqual([], self.calls('SoftLayer_Virtual_PlacementGroup', 'getAvailableRouters')) def test_list_groups(self): result = self.run_command(['vs', 'placementgroup', 'list']) @@ -49,7 +44,7 @@ def test_detail_group_name(self): result = self.run_command(['vs', 'placementgroup', 'detail', 'test']) self.assert_no_fail(result) group_filter = { - 'placementGroups' : { + 'placementGroups': { 'name': {'operation': 'test'} } } @@ -68,7 +63,7 @@ def test_delete_group_name(self, confirm_mock): confirm_mock.return_value = True result = self.run_command(['vs', 'placementgroup', 'delete', 'test']) group_filter = { - 'placementGroups' : { + 'placementGroups': { 'name': {'operation': 'test'} } } @@ -77,7 +72,7 @@ def test_delete_group_name(self, confirm_mock): self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'deleteObject', identifier=12345) @mock.patch('SoftLayer.CLI.formatting.confirm') - def test_delete_group_purge(self,confirm_mock): + 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) @@ -85,7 +80,7 @@ def test_delete_group_purge(self,confirm_mock): self.assert_called_with('SoftLayer_Virtual_Guest', 'deleteObject', identifier=69131875) @mock.patch('SoftLayer.CLI.formatting.confirm') - def test_delete_group_purge_nothing(self,confirm_mock): + def test_delete_group_purge_nothing(self, confirm_mock): group_mock = self.set_mock('SoftLayer_Virtual_PlacementGroup', 'getObject') group_mock.return_value = { "id": 1234, @@ -94,6 +89,6 @@ def test_delete_group_purge_nothing(self,confirm_mock): } confirm_mock.return_value = True result = self.run_command(['vs', 'placementgroup', 'delete', '1234', '--purge']) - self.assertEquals(result.exit_code, 2) + self.assertEqual(result.exit_code, 2) self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'getObject') - self.assertEquals([], self.calls('SoftLayer_Virtual_Guest', 'deleteObject')) + self.assertEqual([], self.calls('SoftLayer_Virtual_Guest', 'deleteObject')) diff --git a/tests/managers/vs/vs_placement_tests.py b/tests/managers/vs/vs_placement_tests.py index 55bbdd987..011c9cfa4 100644 --- a/tests/managers/vs/vs_placement_tests.py +++ b/tests/managers/vs/vs_placement_tests.py @@ -7,18 +7,14 @@ """ import mock -import SoftLayer -from SoftLayer import fixtures -# from SoftLayer.fixtures import SoftLayer_Product_Package -from SoftLayer import testing from SoftLayer.managers.vs_placement import PlacementManager +from SoftLayer import testing class VSPlacementManagerTests(testing.TestCase): def set_up(self): self.manager = PlacementManager(self.client) - amock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') def test_list(self): self.manager.list() @@ -39,7 +35,7 @@ def test_create(self): self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'createObject', args=(placement_object,)) def test_get_object(self): - result = self.manager.get_object(1234) + 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): @@ -54,8 +50,8 @@ def test_delete(self): def test_get_id_from_name(self): self.manager._get_id_from_name('test') _filter = { - 'placementGroups' : { + 'placementGroups': { 'name': {'operation': 'test'} } } - self.assert_called_with('SoftLayer_Account', 'getPlacementGroups', filter=_filter, mask="mask[id, name]") \ No newline at end of file + self.assert_called_with('SoftLayer_Account', 'getPlacementGroups', filter=_filter, mask="mask[id, name]") From c63e4ceee5715cdd4b90b2addbe25db304538b49 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Mon, 28 Jan 2019 18:11:23 -0600 Subject: [PATCH 6/8] #1069 documentation for placement groups --- CONTRIBUTING.md | 10 + Makefile | 192 ++++++++++++++++++++ README.rst | 4 + SoftLayer/CLI/virt/placementgroup/create.py | 6 +- SoftLayer/managers/vs.py | 1 + docs/cli/users.rst | 7 +- docs/cli/vs.rst | 7 + docs/cli/vs/placement_group.rst | 111 +++++++++++ 8 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 Makefile create mode 100644 docs/cli/vs/placement_group.rst 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/virt/placementgroup/create.py b/SoftLayer/CLI/virt/placementgroup/create.py index a6ee49606..8f9776b6b 100644 --- a/SoftLayer/CLI/virt/placementgroup/create.py +++ b/SoftLayer/CLI/virt/placementgroup/create.py @@ -20,10 +20,12 @@ def _get_routers(ctx, _, value): @click.command() @click.option('--name', type=click.STRING, required=True, prompt=True, help="Name for this new placement group.") -@click.option('--backend_router_id', '-b', type=click.INT, required=True, prompt=True, - help="backendRouterId, use --list_routers/-l to print out a list of available ids.") +@click.option('--backend_router', '-b', required=True, prompt=True, + help="backendRouter, can be either the hostname or id.") @click.option('--list_routers', '-l', is_flag=True, callback=_get_routers, is_eager=True, help="Prints available backend router ids and exit.") +@click.option('--rules', '-r', is_flag=True, callback=_get_rules, is_eager=True, + help="Prints available backend router ids and exit.") @environment.pass_env def cli(env, **args): """Create a placement group""" diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 644f80f00..0f5a6d26a 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -876,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', 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..1654f4d7b 100644 --- a/docs/cli/vs.rst +++ b/docs/cli/vs.rst @@ -194,3 +194,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..e74627783 --- /dev/null +++ b/docs/cli/vs/placement_group.rst @@ -0,0 +1,111 @@ +.. _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 backendRouter, can be either the hostname or id. [required] +-h, --help Show this message and exit. + + + +.. _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 + +.. _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 testGroup --purge + + +.. _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 From 3af6f8faf8d37a14736cb195c02dd962a05c7567 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 30 Jan 2019 17:31:36 -0600 Subject: [PATCH 7/8] added a few resolvers for backendrouters, rules, and placementgroups. updated some docs --- SoftLayer/CLI/virt/create.py | 9 +++-- SoftLayer/CLI/virt/placementgroup/create.py | 27 ++++++--------- SoftLayer/managers/__init__.py | 2 +- SoftLayer/managers/vs_placement.py | 22 ++++++++++++ docs/cli/vs.rst | 34 ++++++++++++++----- docs/cli/vs/placement_group.rst | 37 ++++++++++++++++----- 6 files changed, 94 insertions(+), 37 deletions(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index c765831ee..51f8f3675 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -91,7 +91,6 @@ def _parse_create_args(client, args): "datacenter": args.get('datacenter', None), "public_vlan": args.get('vlan_public', None), "private_vlan": args.get('vlan_private', None), - "placement_id": args.get('placement_id', None), "public_subnet": args.get('subnet_public', None), "private_subnet": args.get('subnet_private', None), } @@ -140,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 @@ -192,8 +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('--placement-id', type=click.INT, - help="Placement Group Id to order this guest on. See: slcli vs placementgroup list") +@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/create.py b/SoftLayer/CLI/virt/placementgroup/create.py index 8f9776b6b..951afdacb 100644 --- a/SoftLayer/CLI/virt/placementgroup/create.py +++ b/SoftLayer/CLI/virt/placementgroup/create.py @@ -2,38 +2,31 @@ import click +from SoftLayer import utils from SoftLayer.CLI import environment from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager -def _get_routers(ctx, _, value): - """Prints out the available routers that can be used for placement groups """ - if not value or ctx.resilient_parsing: - return - env = ctx.ensure_object(environment.Environment) - manager = PlacementManager(env.client) - routers = manager.get_routers() - env.fout(get_router_table(routers)) - ctx.exit() - - @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('--list_routers', '-l', is_flag=True, callback=_get_routers, is_eager=True, - help="Prints available backend router ids and exit.") -@click.option('--rules', '-r', is_flag=True, callback=_get_rules, is_eager=True, - help="Prints available backend router ids and exit.") +@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': args.get('backend_router_id'), - 'ruleId': 1 # Hard coded as there is only 1 rule at the moment + 'backendRouterId': backend_router_id, + 'ruleId': rule_id } result = manager.create(placement_object) diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index b6cc1faa5..d70837ca4 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', diff --git a/SoftLayer/managers/vs_placement.py b/SoftLayer/managers/vs_placement.py index acd78c6d9..cabe86d83 100644 --- a/SoftLayer/managers/vs_placement.py +++ b/SoftLayer/managers/vs_placement.py @@ -83,6 +83,28 @@ def delete(self, group_id): """ 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. + """ + from pprint import pprint as pp + results = self.client.call('SoftLayer_Network_Pod', 'getAllObjects') + # pp(results) + 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 = { diff --git a/docs/cli/vs.rst b/docs/cli/vs.rst index 1654f4d7b..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 diff --git a/docs/cli/vs/placement_group.rst b/docs/cli/vs/placement_group.rst index e74627783..c6aa09944 100644 --- a/docs/cli/vs/placement_group.rst +++ b/docs/cli/vs/placement_group.rst @@ -6,18 +6,18 @@ A `Placement Group `_ object, you will need to know the following: -- backendRouterId, from `getAvailableRouters `_) +- 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. +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 + $ 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. @@ -25,7 +25,7 @@ Placement groups can only be deleted once all the virtual guests in the group ha vs placementgroup create ------------------------ -This command will create a placement group +This command will create a placement group. :: @@ -34,9 +34,8 @@ This command will create a placement group Options ^^^^^^^ --name TEXT Name for this new placement group. [required] --b, --backend_router backendRouter, can be either the hostname or id. [required] --h, --help Show this message and exit. - +-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: @@ -48,6 +47,21 @@ This command will print out the available routers and rule sets for use in creat :: $ 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: @@ -67,7 +81,14 @@ You can use the flag --purge to auto-cancel all VSIs in a placement group. You w :: - $ slcli vs placementgroup testGroup --purge + $ 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: From ed7b636fccce280e2f7b4332e86a4ab15a1904ee Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 30 Jan 2019 18:43:05 -0600 Subject: [PATCH 8/8] unit tests and style fixes --- SoftLayer/CLI/helpers.py | 11 ++++-- SoftLayer/CLI/virt/create.py | 2 +- SoftLayer/CLI/virt/placementgroup/create.py | 17 ++------- .../CLI/virt/placementgroup/create_options.py | 38 +++++++++++++++++++ SoftLayer/fixtures/SoftLayer_Account.py | 5 ++- .../SoftLayer_Virtual_PlacementGroup_Rule.py | 7 ++++ SoftLayer/managers/__init__.py | 1 + SoftLayer/managers/vs_placement.py | 8 ++-- tests/CLI/modules/vs/vs_capacity_tests.py | 21 ++++++++++ tests/CLI/modules/vs/vs_placement_tests.py | 23 +++++++++-- tests/managers/vs/vs_capacity_tests.py | 10 +++++ tests/managers/vs/vs_placement_tests.py | 20 ++++++++++ 12 files changed, 134 insertions(+), 29 deletions(-) create mode 100644 SoftLayer/CLI/virt/placementgroup/create_options.py create mode 100644 SoftLayer/fixtures/SoftLayer_Virtual_PlacementGroup_Rule.py 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/virt/create.py b/SoftLayer/CLI/virt/create.py index 51f8f3675..631793475 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -195,7 +195,7 @@ 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', +@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 diff --git a/SoftLayer/CLI/virt/placementgroup/create.py b/SoftLayer/CLI/virt/placementgroup/create.py index 951afdacb..af1fb8db5 100644 --- a/SoftLayer/CLI/virt/placementgroup/create.py +++ b/SoftLayer/CLI/virt/placementgroup/create.py @@ -2,9 +2,7 @@ import click -from SoftLayer import utils from SoftLayer.CLI import environment -from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers from SoftLayer.managers.vs_placement import PlacementManager as PlacementManager @@ -19,10 +17,10 @@ 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'), + 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') + 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, @@ -31,12 +29,3 @@ def cli(env, **args): result = manager.create(placement_object) click.secho("Successfully created placement group: ID: %s, Name: %s" % (result['id'], result['name']), fg='green') - - -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 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/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index b01be4083..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,6 +638,9 @@ 'description': 'B1.1x2 (1 Year Term)', 'hourlyRecurringFee': '.032' } + }, + { + 'id': 3519 } ] } 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 d70837ca4..02e54b30e 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -47,6 +47,7 @@ 'NetworkManager', 'ObjectStorageManager', 'OrderingManager', + 'PlacementManager', 'SshKeyManager', 'SSLManager', 'TicketManager', diff --git a/SoftLayer/managers/vs_placement.py b/SoftLayer/managers/vs_placement.py index cabe86d83..d40a845e9 100644 --- a/SoftLayer/managers/vs_placement.py +++ b/SoftLayer/managers/vs_placement.py @@ -87,22 +87,20 @@ 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): + 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()] + return [result['id'] for result in results if result['keyName'] == name.upper()] - def _get_backend_router_id_from_hostname(self, hostname): + 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. """ - from pprint import pprint as pp results = self.client.call('SoftLayer_Network_Pod', 'getAllObjects') - # pp(results) return [result['backendRouterId'] for result in results if result['backendRouterName'] == hostname.lower()] def _get_id_from_name(self, name): 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 index ecff4f58f..3b716a6cd 100644 --- a/tests/CLI/modules/vs/vs_placement_tests.py +++ b/tests/CLI/modules/vs/vs_placement_tests.py @@ -11,20 +11,21 @@ class VSPlacementTests(testing.TestCase): - def test_create_group_list_routers(self): - result = self.run_command(['vs', 'placementgroup', 'create', '--list_routers']) + 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_id=1']) + result = self.run_command(['vs', 'placementgroup', 'create', '--name=test', '--backend_router=1', '--rule=2']) create_args = { 'name': 'test', 'backendRouterId': 1, - 'ruleId': 1 + 'ruleId': 2 } self.assert_no_fail(result) self.assert_called_with('SoftLayer_Virtual_PlacementGroup', 'createObject', args=(create_args,)) @@ -58,6 +59,13 @@ def test_delete_group_id(self, confirm_mock): 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 @@ -79,6 +87,13 @@ def test_delete_group_purge(self, confirm_mock): 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') 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 index 011c9cfa4..b492f69bf 100644 --- a/tests/managers/vs/vs_placement_tests.py +++ b/tests/managers/vs/vs_placement_tests.py @@ -55,3 +55,23 @@ def test_get_id_from_name(self): } } 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, [])