diff --git a/README.rst b/README.rst index 53f5c2b53..c543ce5ae 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ For the CLI, just use the -vvv option. If you are using the REST endpoint, this If you are using the library directly in python, you can do something like this. -.. code-bock:: python +.. code-block:: python import SoftLayer import logging diff --git a/SoftLayer/CLI/autoscale/__init__.py b/SoftLayer/CLI/autoscale/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/SoftLayer/CLI/autoscale/detail.py b/SoftLayer/CLI/autoscale/detail.py new file mode 100644 index 000000000..337be43a6 --- /dev/null +++ b/SoftLayer/CLI/autoscale/detail.py @@ -0,0 +1,92 @@ +"""Get details of an Autoscale groups.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.autoscale import AutoScaleManager +from SoftLayer import utils + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """Get details of an Autoscale groups.""" + + autoscale = AutoScaleManager(env.client) + group = autoscale.details(identifier) + + # Group Config Table + table = formatting.KeyValueTable(["Group", "Value"]) + table.align['Group'] = 'l' + table.align['Value'] = 'l' + + table.add_row(['Id', group.get('id')]) + # Ideally we would use regionalGroup->preferredDatacenter, but that generates an error. + table.add_row(['Datacenter', group['regionalGroup']['locations'][0]['longName']]) + table.add_row(['Termination', utils.lookup(group, 'terminationPolicy', 'name')]) + table.add_row(['Minimum Members', group.get('minimumMemberCount')]) + table.add_row(['Maximum Members', group.get('maximumMemberCount')]) + table.add_row(['Current Members', group.get('virtualGuestMemberCount')]) + table.add_row(['Cooldown', "{} seconds".format(group.get('cooldown'))]) + table.add_row(['Last Action', utils.clean_time(group.get('lastActionDate'))]) + + for network in group.get('networkVlans', []): + network_type = utils.lookup(network, 'networkVlan', 'networkSpace') + router = utils.lookup(network, 'networkVlan', 'primaryRouter', 'hostname') + vlan_number = utils.lookup(network, 'networkVlan', 'vlanNumber') + vlan_name = "{}.{}".format(router, vlan_number) + table.add_row([network_type, vlan_name]) + + env.fout(table) + + # Template Config Table + config_table = formatting.KeyValueTable(["Template", "Value"]) + config_table.align['Template'] = 'l' + config_table.align['Value'] = 'l' + + template = group.get('virtualGuestMemberTemplate') + + config_table.add_row(['Hostname', template.get('hostname')]) + config_table.add_row(['Domain', template.get('domain')]) + config_table.add_row(['Core', template.get('startCpus')]) + config_table.add_row(['Ram', template.get('maxMemory')]) + network = template.get('networkComponents') + config_table.add_row(['Network', network[0]['maxSpeed'] if network else 'Default']) + ssh_keys = template.get('sshKeys', []) + ssh_manager = SoftLayer.SshKeyManager(env.client) + for key in ssh_keys: + # Label isn't included when retrieved from the AutoScale group... + ssh_key = ssh_manager.get_key(key.get('id')) + config_table.add_row(['SSH Key {}'.format(ssh_key.get('id')), ssh_key.get('label')]) + disks = template.get('blockDevices', []) + disk_type = "Local" if template.get('localDiskFlag') else "SAN" + + for disk in disks: + disk_image = disk.get('diskImage') + config_table.add_row(['{} Disk {}'.format(disk_type, disk.get('device')), disk_image.get('capacity')]) + config_table.add_row(['OS', template.get('operatingSystemReferenceCode')]) + config_table.add_row(['Post Install', template.get('postInstallScriptUri') or 'None']) + + env.fout(config_table) + + # Policy Config Table + policy_table = formatting.KeyValueTable(["Policy", "Cooldown"]) + policies = group.get('policies', []) + for policy in policies: + policy_table.add_row([policy.get('name'), policy.get('cooldown') or group.get('cooldown')]) + + env.fout(policy_table) + + # Active Guests + member_table = formatting.Table(['Id', 'Hostname', 'Created'], title="Active Guests") + guests = group.get('virtualGuestMembers', []) + for guest in guests: + real_guest = guest.get('virtualGuest') + member_table.add_row([ + real_guest.get('id'), real_guest.get('hostname'), utils.clean_time(real_guest.get('provisionDate')) + ]) + env.fout(member_table) diff --git a/SoftLayer/CLI/autoscale/edit.py b/SoftLayer/CLI/autoscale/edit.py new file mode 100644 index 000000000..c470aebbf --- /dev/null +++ b/SoftLayer/CLI/autoscale/edit.py @@ -0,0 +1,57 @@ +"""Edits an Autoscale group.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.managers.autoscale import AutoScaleManager + + +@click.command() +@click.argument('identifier') +@click.option('--name', help="Scale group's name.") +@click.option('--min', 'minimum', type=click.INT, help="Set the minimum number of guests") +@click.option('--max', 'maximum', type=click.INT, help="Set the maximum number of guests") +@click.option('--userdata', help="User defined metadata string") +@click.option('--userfile', '-F', help="Read userdata from a file", + type=click.Path(exists=True, readable=True, resolve_path=True)) +@click.option('--cpu', type=click.INT, help="Number of CPUs for new guests (existing not effected") +@click.option('--memory', type=click.INT, help="RAM in MB or GB for new guests (existing not effected") +@environment.pass_env +def cli(env, identifier, name, minimum, maximum, userdata, userfile, cpu, memory): + """Edits an Autoscale group.""" + + template = {} + autoscale = AutoScaleManager(env.client) + group = autoscale.details(identifier) + + template['name'] = name + template['minimumMemberCount'] = minimum + template['maximumMemberCount'] = maximum + virt_template = {} + if userdata: + virt_template['userData'] = [{"value": userdata}] + elif userfile: + with open(userfile, 'r') as userfile_obj: + virt_template['userData'] = [{"value": userfile_obj.read()}] + virt_template['startCpus'] = cpu + virt_template['maxMemory'] = memory + + # Remove any entries that are `None` as the API will complain about them. + template['virtualGuestMemberTemplate'] = clean_dict(virt_template) + clean_template = clean_dict(template) + + # If there are any values edited in the template, we need to get the OLD template values and replace them. + if template['virtualGuestMemberTemplate']: + # Update old template with new values + for key, value in clean_template['virtualGuestMemberTemplate'].items(): + group['virtualGuestMemberTemplate'][key] = value + clean_template['virtualGuestMemberTemplate'] = group['virtualGuestMemberTemplate'] + + autoscale.edit(identifier, clean_template) + click.echo("Done") + + +def clean_dict(dictionary): + """Removes any `None` entires from the dictionary""" + return {k: v for k, v in dictionary.items() if v} diff --git a/SoftLayer/CLI/autoscale/list.py b/SoftLayer/CLI/autoscale/list.py new file mode 100644 index 000000000..2d360714c --- /dev/null +++ b/SoftLayer/CLI/autoscale/list.py @@ -0,0 +1,29 @@ +"""List Autoscale groups.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.autoscale import AutoScaleManager +from SoftLayer import utils + + +@click.command() +@environment.pass_env +def cli(env): + """List AutoScale Groups.""" + + autoscale = AutoScaleManager(env.client) + groups = autoscale.list() + + table = formatting.Table(["Id", "Name", "Status", "Min/Max", "Running"]) + table.align['Name'] = 'l' + for group in groups: + status = utils.lookup(group, 'status', 'name') + min_max = "{}/{}".format(group.get('minimumMemberCount'), group.get('maximumMemberCount')) + table.add_row([ + group.get('id'), group.get('name'), status, min_max, group.get('virtualGuestMemberCount') + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/autoscale/logs.py b/SoftLayer/CLI/autoscale/logs.py new file mode 100644 index 000000000..6c4c401b3 --- /dev/null +++ b/SoftLayer/CLI/autoscale/logs.py @@ -0,0 +1,37 @@ +"""Retreive logs for an autoscale group""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.autoscale import AutoScaleManager +from SoftLayer import utils + + +@click.command() +@click.argument('identifier') +@click.option('--date-min', '-d', 'date_min', type=click.DateTime(formats=["%Y-%m-%d", "%m/%d/%Y"]), + help='Earliest date to retreive logs for.') +@environment.pass_env +def cli(env, identifier, date_min): + """Retreive logs for an autoscale group""" + + autoscale = AutoScaleManager(env.client) + + mask = "mask[id,createDate,description]" + object_filter = {} + if date_min: + object_filter['logs'] = { + 'createDate': { + 'operation': 'greaterThanDate', + 'options': [{'name': 'date', 'value': [date_min.strftime("%m/%d/%Y")]}] + } + } + + logs = autoscale.get_logs(identifier, mask=mask, object_filter=object_filter) + table = formatting.Table(['Date', 'Entry'], title="Logs") + table.align['Entry'] = 'l' + for log in logs: + table.add_row([utils.clean_time(log.get('createDate')), log.get('description')]) + env.fout(table) diff --git a/SoftLayer/CLI/autoscale/scale.py b/SoftLayer/CLI/autoscale/scale.py new file mode 100644 index 000000000..69fe1305a --- /dev/null +++ b/SoftLayer/CLI/autoscale/scale.py @@ -0,0 +1,55 @@ +"""Scales an Autoscale group""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.autoscale import AutoScaleManager +from SoftLayer import utils + + +@click.command() +@click.argument('identifier') +@click.option('--up/--down', 'scale_up', is_flag=True, default=True, + help="'--up' adds guests, '--down' removes guests.") +@click.option('--by/--to', 'scale_by', is_flag=True, required=True, + help="'--by' will add/remove the specified number of guests." + " '--to' will add/remove a number of guests to get the group's guest count to the specified number.") +@click.option('--amount', required=True, type=click.INT, help="Number of guests for the scale action.") +@environment.pass_env +def cli(env, identifier, scale_up, scale_by, amount): + """Scales an Autoscale group. Bypasses a scale group's cooldown period.""" + + autoscale = AutoScaleManager(env.client) + + # Scale By, and go down, need to use negative amount + if not scale_up and scale_by: + amount = amount * -1 + + result = [] + if scale_by: + click.secho("Scaling group {} by {}".format(identifier, amount), fg='green') + result = autoscale.scale(identifier, amount) + else: + click.secho("Scaling group {} to {}".format(identifier, amount), fg='green') + result = autoscale.scale_to(identifier, amount) + + try: + # Check if the first guest has a cancellation date, assume we are removing guests if it is. + cancel_date = result[0]['virtualGuest']['billingItem']['cancellationDate'] or False + except (IndexError, KeyError, TypeError): + cancel_date = False + + if cancel_date: + member_table = formatting.Table(['Id', 'Hostname', 'Created'], title="Cancelled Guests") + else: + member_table = formatting.Table(['Id', 'Hostname', 'Created'], title="Added Guests") + + for guest in result: + real_guest = guest.get('virtualGuest') + member_table.add_row([ + guest.get('id'), real_guest.get('hostname'), utils.clean_time(real_guest.get('createDate')) + ]) + + env.fout(member_table) diff --git a/SoftLayer/CLI/autoscale/tag.py b/SoftLayer/CLI/autoscale/tag.py new file mode 100644 index 000000000..58e4101b7 --- /dev/null +++ b/SoftLayer/CLI/autoscale/tag.py @@ -0,0 +1,33 @@ +"""Tags all guests in an autoscale group.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.managers.autoscale import AutoScaleManager +from SoftLayer.managers.vs import VSManager + + +@click.command() +@click.argument('identifier') +@click.option('--tags', '-g', help="Tags to set for each guest in this group. Existing tags are overwritten. " + "An empty string will remove all tags") +@environment.pass_env +def cli(env, identifier, tags): + """Tags all guests in an autoscale group. + + --tags "Use, quotes, if you, want whitespace" + + --tags Otherwise,Just,commas + """ + + autoscale = AutoScaleManager(env.client) + vsmanager = VSManager(env.client) + mask = "mask[id,virtualGuestId,virtualGuest[tagReferences,id,hostname]]" + guests = autoscale.get_virtual_guests(identifier, mask=mask) + click.echo("New Tags: {}".format(tags)) + for guest in guests: + real_guest = guest.get('virtualGuest') + click.echo("Setting tags for {}".format(real_guest.get('hostname'))) + vsmanager.set_tags(tags, real_guest.get('id'),) + click.echo("Done") diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index fb33ee9e2..5c37541df 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -302,6 +302,14 @@ ('report', 'SoftLayer.CLI.report'), ('report:bandwidth', 'SoftLayer.CLI.report.bandwidth:cli'), + + ('autoscale', 'SoftLayer.CLI.autoscale'), + ('autoscale:list', 'SoftLayer.CLI.autoscale.list:cli'), + ('autoscale:detail', 'SoftLayer.CLI.autoscale.detail:cli'), + ('autoscale:scale', 'SoftLayer.CLI.autoscale.scale:cli'), + ('autoscale:logs', 'SoftLayer.CLI.autoscale.logs:cli'), + ('autoscale:tag', 'SoftLayer.CLI.autoscale.tag:cli'), + ('autoscale:edit', 'SoftLayer.CLI.autoscale.edit:cli') ] ALL_ALIASES = { diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index a0c865e3d..ddb2a4354 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -769,3 +769,86 @@ } } ] + +getScaleGroups = [ + { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2016-10-25T01:48:34+08:00", + "id": 12222222, + "lastActionDate": "2016-10-25T01:48:34+08:00", + "maximumMemberCount": 5, + "minimumMemberCount": 0, + "name": "tests", + "regionalGroupId": 663, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "sodg.com", + "hostname": "testing", + "id": None, + "maxCpu": None, + "maxMemory": 32768, + "startCpus": 32, + "blockDevices": [ + { + "device": "0", + "diskImage": { + "capacity": 25, + } + } + ], + "datacenter": { + "name": "sao01", + }, + "hourlyBillingFlag": True, + "operatingSystemReferenceCode": "CENTOS_LATEST", + }, + "virtualGuestMemberCount": 0, + "status": { + "id": 1, + "keyName": "ACTIVE", + "name": "Active" + }, + }, + { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2018-04-24T04:22:00+08:00", + "id": 224533333, + "lastActionDate": "2019-01-19T04:53:18+08:00", + "maximumMemberCount": 10, + "minimumMemberCount": 0, + "modifyDate": "2019-01-19T04:53:21+08:00", + "name": "test-ajcb", + "regionalGroupId": 1025, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "test.local", + "hostname": "autoscale-ajcb01", + "id": None, + "maxCpu": None, + "maxMemory": 1024, + "postInstallScriptUri": "http://test.com", + "startCpus": 1, + "blockDevices": [ + { + "device": "0", + "diskImage": { + "capacity": 25, + } + } + ], + "datacenter": { + "name": "seo01", + }, + "hourlyBillingFlag": True, + "operatingSystemReferenceCode": "CENTOS_7_64", + }, + "virtualGuestMemberCount": 0, + "status": { + "id": 1, + "keyName": "ACTIVE", + "name": "Active" + } + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Scale_Group.py b/SoftLayer/fixtures/SoftLayer_Scale_Group.py new file mode 100644 index 000000000..f04d8f56e --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Scale_Group.py @@ -0,0 +1,457 @@ +getObject = { + 'accountId': 31111, + 'balancedTerminationFlag': False, + 'cooldown': 1800, + 'createDate': '2018-04-30T15:07:40-04:00', + 'desiredMemberCount': None, + 'id': 12222222, + 'lastActionDate': '2019-10-02T16:26:17-04:00', + 'loadBalancers': [], + 'maximumMemberCount': 6, + 'minimumMemberCount': 2, + 'modifyDate': '2019-10-03T17:16:47-04:00', + 'name': 'tests', + 'networkVlans': [ + { + 'networkVlan': { + 'accountId': 31111, + 'id': 2222222, + 'modifyDate': '2019-07-16T13:09:47-04:00', + 'networkSpace': 'PRIVATE', + 'primaryRouter': { + 'hostname': 'bcr01a.sao01' + }, + 'primarySubnetId': 1172222, + 'vlanNumber': 1111 + }, + 'networkVlanId': 2281111 + } + ], + 'policies': [ + { + 'actions': [ + { + 'amount': 1, + 'createDate': '2019-09-26T18:30:22-04:00', + 'deleteFlag': None, + 'id': 611111, + 'modifyDate': None, + 'scalePolicy': None, + 'scalePolicyId': 681111, + 'scaleType': 'RELATIVE', + 'typeId': 1 + } + ], + 'cooldown': None, + 'createDate': '2019-09-26T18:30:14-04:00', + 'id': 680000, + 'name': 'prime-poly', + 'scaleActions': [ + { + 'amount': 1, + 'createDate': '2019-09-26T18:30:22-04:00', + 'deleteFlag': None, + 'id': 633333, + 'modifyDate': None, + 'scalePolicy': None, + 'scalePolicyId': 680123, + 'scaleType': 'RELATIVE', + 'typeId': 1 + } + ], + 'triggers': [ + { + 'createDate': '2019-09-26T18:30:14-04:00', + 'deleteFlag': None, + 'id': 557111, + 'modifyDate': None, + 'scalePolicy': None, + 'scalePolicyId': 680000, + 'typeId': 3 + } + ] + } + ], + 'regionalGroup': { + 'description': 'sa-bra-south-1', + 'id': 663, + 'locationGroupTypeId': 42, + 'locations': [ + { + 'id': 983497, + 'longName': 'Sao Paulo 1', + 'name': 'sao01', + 'statusId': 2 + } + ], + 'name': 'sa-bra-south-1', + 'securityLevelId': None + }, + 'regionalGroupId': 663, + 'status': { + 'id': 1, 'keyName': 'ACTIVE', 'name': 'Active' + }, + 'suspendedFlag': False, + 'terminationPolicy': { + 'id': 2, 'keyName': 'NEWEST', 'name': 'Newest' + }, + 'terminationPolicyId': 2, + 'virtualGuestAssets': [], + 'virtualGuestMemberCount': 6, + 'virtualGuestMemberTemplate': { + 'accountId': 31111, + 'blockDevices': [ + { + 'bootableFlag': None, + 'createDate': None, + 'device': '0', + 'diskImage': { + 'capacity': 25, + 'createDate': None, + 'id': None, + 'modifyDate': None, + 'parentId': None, + 'storageRepositoryId': None, + 'typeId': None}, + 'diskImageId': None, + 'guestId': None, + 'hotPlugFlag': None, + 'id': None, + 'modifyDate': None, + 'statusId': None + }, + { + 'bootableFlag': None, + 'createDate': None, + 'device': '2', + 'diskImage': { + 'capacity': 10, + 'createDate': None, + 'id': None, + 'modifyDate': None, + 'parentId': None, + 'storageRepositoryId': None, + 'typeId': None}, + 'diskImageId': None, + 'guestId': None, + 'hotPlugFlag': None, + 'id': None, + 'modifyDate': None, + 'statusId': None + } + ], + 'createDate': None, + 'datacenter': { + 'id': None, + 'name': 'sao01', + 'statusId': None + }, + 'dedicatedAccountHostOnlyFlag': None, + 'domain': 'tech-support.com', + 'hostname': 'testing', + 'hourlyBillingFlag': True, + 'id': None, + 'lastPowerStateId': None, + 'lastVerifiedDate': None, + 'localDiskFlag': False, + 'maxCpu': None, + 'maxMemory': 1024, + 'metricPollDate': None, + 'modifyDate': None, + 'networkComponents': [ + { + 'createDate': None, + 'guestId': None, + 'id': None, + 'maxSpeed': 100, + 'modifyDate': None, + 'networkId': None, + 'port': None, + 'speed': None + } + ], + 'operatingSystemReferenceCode': 'CENTOS_LATEST', + 'placementGroupId': None, + 'postInstallScriptUri': 'https://test.com/', + 'privateNetworkOnlyFlag': False, + 'provisionDate': None, + 'sshKeys': [ + { + 'createDate': None, + 'id': 490279, + 'modifyDate': None + } + ], + 'startCpus': 1, + 'statusId': None, + 'typeId': None}, + 'virtualGuestMembers': [ + { + 'id': 3111111, + 'virtualGuest': { + + 'domain': 'tech-support.com', + 'hostname': 'test', + 'provisionDate': '2019-09-27T14:29:53-04:00' + } + } + ] +} + +getVirtualGuestMembers = getObject['virtualGuestMembers'] + +scale = [ + { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2016-10-25T01:48:34+08:00", + "id": 12222222, + "maximumMemberCount": 5, + "minimumMemberCount": 0, + "name": "tests", + "virtualGuest": { + "accountId": 31111, + "createDate": "2019-10-02T15:24:54-06:00", + "billingItem": { + "cancellationDate": "2019-10-02T08:34:21-06:00"} + }, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "sodg.com", + "hostname": "testing", + "maxMemory": 32768, + "startCpus": 32, + "blockDevices": [ + { + "device": "0", + "diskImage": { + "capacity": 25, + } + } + ], + "datacenter": { + "name": "sao01", + }, + "hourlyBillingFlag": True, + "operatingSystemReferenceCode": "CENTOS_LATEST", + "privateNetworkOnlyFlag": True + }, + "virtualGuestMemberCount": 0, + "status": { + "id": 1, + "keyName": "ACTIVE", + "name": "Active" + }, + "virtualGuestAssets": [], + "virtualGuestMembers": [] + }, + { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2018-04-24T04:22:00+08:00", + "id": 224533333, + "maximumMemberCount": 10, + "minimumMemberCount": 0, + "modifyDate": "2019-01-19T04:53:21+08:00", + "name": "test-ajcb", + "virtualGuest": { + "accountId": 31111, + "createDate": "2019-10-02T15:24:54-06:00", + "billingItem": { + "cancellationDate": "2019-10-02T08:34:21-06:00"} + }, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "test.local", + "hostname": "autoscale-ajcb01", + "id": None, + "maxCpu": None, + "maxMemory": 1024, + "postInstallScriptUri": "http://test.com", + "startCpus": 1, + "blockDevices": [ + { + "device": "0", + "diskImage": { + "capacity": 25, + } + } + ], + "datacenter": { + "name": "seo01", + }, + "hourlyBillingFlag": True, + "operatingSystemReferenceCode": "CENTOS_7_64", + }, + "virtualGuestMemberCount": 0, + "status": { + "id": 1, + "keyName": "ACTIVE", + "name": "Active" + }, + "virtualGuestAssets": [], + "virtualGuestMembers": [] + } +] + +scaleTo = [ + { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2016-10-25T01:48:34+08:00", + "id": 12222222, + "lastActionDate": "2016-10-25T01:48:34+08:00", + "maximumMemberCount": 5, + "minimumMemberCount": 0, + "name": "tests", + "regionalGroupId": 663, + "virtualGuest": { + }, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "sodg.com", + "hostname": "testing", + "id": None, + "maxCpu": None, + "maxMemory": 32768, + "startCpus": 32, + "datacenter": { + "name": "sao01", + }, + "hourlyBillingFlag": True, + "operatingSystemReferenceCode": "CENTOS_LATEST", + "privateNetworkOnlyFlag": True + }, + "virtualGuestMemberCount": 0, + "status": { + "id": 1, + "keyName": "ACTIVE", + "name": "Active" + }, + "virtualGuestAssets": [], + "virtualGuestMembers": [] + }, + { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2018-04-24T04:22:00+08:00", + "id": 224533333, + "lastActionDate": "2019-01-19T04:53:18+08:00", + "maximumMemberCount": 10, + "minimumMemberCount": 0, + "modifyDate": "2019-01-19T04:53:21+08:00", + "name": "test-ajcb", + "regionalGroupId": 1025, + "virtualGuest": { + "accountId": 31111, + "createDate": "2019-10-02T15:24:54-06:00", + "billingItem": { + "cancellationDate": "2019-10-02T08:34:21-06:00"} + }, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "test.local", + "hostname": "autoscale-ajcb01", + "id": None, + "maxCpu": None, + "maxMemory": 1024, + "postInstallScriptUri": "http://test.com", + "startCpus": 1, + "blockDevices": [ + { + "device": "0", + "diskImage": { + "capacity": 25, + } + } + ], + "datacenter": { + "name": "seo01", + }, + "hourlyBillingFlag": True, + "operatingSystemReferenceCode": "CENTOS_7_64", + }, + "virtualGuestMemberCount": 0, + "status": { + "id": 1, + "keyName": "ACTIVE", + "name": "Active" + }, + "virtualGuestAssets": [], + "virtualGuestMembers": [] + }, +] + +getLogs = [ + { + "createDate": "2019-10-03T04:26:11+08:00", + "description": "Scaling group to 6 member(s) by adding 3 member(s) as manually requested", + "id": 3821111, + "scaleGroupId": 2252222, + "scaleGroup": { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2018-05-01T03:07:40+08:00", + "id": 2251111, + "lastActionDate": "2019-10-03T04:26:17+08:00", + "maximumMemberCount": 6, + "minimumMemberCount": 2, + "modifyDate": "2019-10-03T04:26:21+08:00", + "name": "ajcb-autoscale11", + "regionalGroupId": 663, + "terminationPolicyId": 2, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "techsupport.com", + "hostname": "ajcb-autoscale22", + "maxMemory": 1024, + "postInstallScriptUri": "https://pastebin.com/raw/62wrEKuW", + "startCpus": 1, + "blockDevices": [ + { + "device": "0", + "diskImage": { + "capacity": 25, + } + }, + { + "device": "2", + "diskImage": { + "capacity": 10, + } + } + ], + "datacenter": { + "name": "sao01", + }, + "networkComponents": [ + { + "maxSpeed": 100, + } + ], + "operatingSystemReferenceCode": "CENTOS_LATEST", + "sshKeys": [ + { + "id": 49111, + } + ] + }, + "logs": [ + { + "createDate": "2019-09-28T02:31:35+08:00", + "description": "Scaling group to 3 member(s) by removing -1 member(s) as manually requested", + "id": 3821111, + "scaleGroupId": 2251111, + }, + { + "createDate": "2019-09-28T02:26:11+08:00", + "description": "Scaling group to 4 member(s) by adding 2 member(s) as manually requested", + "id": 38211111, + "scaleGroupId": 2251111, + }, + ] + } + }, +] + +editObject = True diff --git a/SoftLayer/fixtures/SoftLayer_Scale_Policy.py b/SoftLayer/fixtures/SoftLayer_Scale_Policy.py new file mode 100644 index 000000000..5586fec1f --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Scale_Policy.py @@ -0,0 +1,58 @@ +getObject = { + "cooldown": None, + "createDate": "2019-09-27T06:30:14+08:00", + "id": 11111, + "name": "prime-poly", + "scaleGroupId": 2255111, + "scaleGroup": { + "accountId": 31111, + "cooldown": 1800, + "createDate": "2018-05-01T03:07:40+08:00", + "id": 2252222, + "lastActionDate": "2019-09-28T02:31:47+08:00", + "maximumMemberCount": 6, + "minimumMemberCount": 2, + "modifyDate": "2019-09-28T02:31:50+08:00", + "name": "ajcb-autoscale11", + "regionalGroupId": 663, + "terminationPolicyId": 2, + "virtualGuestMemberTemplate": { + "accountId": 31111, + "domain": "techsupport.com", + "hostname": "ajcb-autoscale22", + "id": None, + "maxMemory": 1024, + "postInstallScriptUri": "https://pastebin.com/raw/62wrEKuW", + "startCpus": 1, + "blockDevices": [ + { + "device": "0", + "diskImage": { + "capacity": 25, + } + }, + { + "device": "2", + "diskImage": { + "capacity": 10, + } + } + ], + "datacenter": { + "id": None, + "name": "sao01", + }, + "networkComponents": [ + { + "maxSpeed": 100, + } + ], + "operatingSystemReferenceCode": "CENTOS_LATEST", + "sshKeys": [ + { + "id": 490279, + } + ] + } + } +} diff --git a/SoftLayer/managers/autoscale.py b/SoftLayer/managers/autoscale.py new file mode 100644 index 000000000..40d7ebe80 --- /dev/null +++ b/SoftLayer/managers/autoscale.py @@ -0,0 +1,116 @@ +""" + SoftLayer.autoscale + ~~~~~~~~~~~~~~~~~~~ + Autoscale manager + + :license: MIT, see LICENSE for more details. +""" + + +class AutoScaleManager(object): + """Manager for interacting with Autoscale instances.""" + + def __init__(self, client): + self.client = client + + def list(self, mask=None): + """Calls `SoftLayer_Account::getScaleGroups()`_ + + :param mask: optional SoftLayer_Scale_Group objectMask + + .. _SoftLayer_Account::getScaleGroups(): + https://sldn.softlayer.com/reference/services/SoftLayer_Account/getScaleGroups/ + """ + if not mask: + mask = "mask[status,virtualGuestMemberCount]" + + return self.client.call('SoftLayer_Account', 'getScaleGroups', mask=mask, iter=True) + + def details(self, identifier, mask=None): + """Calls `SoftLayer_Scale_Group::getObject()`_ + + :param identifier: SoftLayer_Scale_Group id + :param mask: optional SoftLayer_Scale_Group objectMask + + .. _SoftLayer_Scale_Group::getObject(): + https://sldn.softlayer.com/reference/services/SoftLayer_Scale_Group/getObject/ + """ + if not mask: + mask = """mask[virtualGuestMembers[id,virtualGuest[id,hostname,domain,provisionDate]], terminationPolicy, + virtualGuestMemberCount, virtualGuestMemberTemplate[sshKeys], + policies[id,name,createDate,cooldown,actions,triggers,scaleActions], + networkVlans[networkVlanId,networkVlan[networkSpace,primaryRouter[hostname]]], + loadBalancers, regionalGroup[locations]]""" + return self.client.call('SoftLayer_Scale_Group', 'getObject', id=identifier, mask=mask) + + def get_policy(self, identifier, mask=None): + """Calls `SoftLayer_Scale_Policy::getObject()`_ + + :param identifier: SoftLayer_Scale_Policy id + :param mask: optional SoftLayer_Scale_Policy objectMask + + .. _SoftLayer_Scale_Policy::getObject(): + https://sldn.softlayer.com/reference/services/SoftLayer_Scale_Policy/getObject/ + """ + if not mask: + mask = """mask[cooldown, createDate, id, name, actions, triggers[type] + + ]""" + + return self.client.call('SoftLayer_Scale_Policy', 'getObject', id=identifier, mask=mask) + + def scale(self, identifier, amount): + """Calls `SoftLayer_Scale_Group::scale()`_ + + :param identifier: SoftLayer_Scale_Group Id + :param amount: positive or negative number to scale the group by + + .. _SoftLayer_Scale_Group::scale(): + https://sldn.softlayer.com/reference/services/SoftLayer_Scale_Group/scale/ + """ + return self.client.call('SoftLayer_Scale_Group', 'scale', amount, id=identifier) + + def scale_to(self, identifier, amount): + """Calls `SoftLayer_Scale_Group::scaleTo()`_ + + :param identifier: SoftLayer_Scale_Group Id + :param amount: number to scale the group to. + + .. _SoftLayer_Scale_Group::scaleTo(): + https://sldn.softlayer.com/reference/services/SoftLayer_Scale_Group/scaleTo/ + """ + return self.client.call('SoftLayer_Scale_Group', 'scaleTo', amount, id=identifier) + + def get_logs(self, identifier, mask=None, object_filter=None): + """Calls `SoftLayer_Scale_Group::getLogs()`_ + + :param identifier: SoftLayer_Scale_Group Id + :param mask: optional SoftLayer_Scale_Group_Log objectMask + :param object_filter: optional SoftLayer_Scale_Group_Log objectFilter + + .. _SoftLayer_Scale_Group::getLogs(): + https://sldn.softlayer.com/reference/services/SoftLayer_Scale_Group/getLogs/ + """ + return self.client.call('SoftLayer_Scale_Group', 'getLogs', id=identifier, mask=mask, filter=object_filter, + iter=True) + + def get_virtual_guests(self, identifier, mask=None): + """Calls `SoftLayer_Scale_Group::getVirtualGuestMembers()`_ + + :param identifier: SoftLayer_Scale_Group Id + :param mask: optional SoftLayer_Scale_Member objectMask + .. _SoftLayer_Scale_Group::getVirtualGuestMembers(): + https://sldn.softlayer.com/reference/services/SoftLayer_Scale_Group/getVirtualGuestMembers/ + """ + return self.client.call('SoftLayer_Scale_Group', 'getVirtualGuestMembers', id=identifier, mask=mask, iter=True) + + def edit(self, identifier, template): + """Calls `SoftLayer_Scale_Group::editObject()`_ + + :param identifier: SoftLayer_Scale_Group id + :param template: `SoftLayer_Scale_Group`_ + .. _SoftLayer_Scale_Group::editObject(): + https://sldn.softlayer.com/reference/services/SoftLayer_Scale_Group/editObject/ + .. _SoftLayer_Scale_Group: https://sldn.softlayer.com/reference/datatypes/SoftLayer_Scale_Group/ + """ + return self.client.call('SoftLayer_Scale_Group', 'editObject', template, id=identifier) diff --git a/docs/api/managers/autoscale.rst b/docs/api/managers/autoscale.rst new file mode 100644 index 000000000..3a617c244 --- /dev/null +++ b/docs/api/managers/autoscale.rst @@ -0,0 +1,5 @@ +.. _autoscale: + +.. automodule:: SoftLayer.managers.autoscale + :members: + :inherited-members: \ No newline at end of file diff --git a/docs/cli/autoscale.rst b/docs/cli/autoscale.rst new file mode 100644 index 000000000..a3aa31462 --- /dev/null +++ b/docs/cli/autoscale.rst @@ -0,0 +1,38 @@ +.. _cli_autoscale: + +Autoscale Commands +================== +These commands were added in version `5.8.1 `_ + +For making changes to the triggers or the autoscale group itself, see the `Autoscale Portal`_ + +- `Autoscale Product `_ +- `Autoscale Documentation `_ +- `Autoscale Portal`_ + +.. click:: SoftLayer.CLI.autoscale.list:cli + :prog: autoscale list + :show-nested: + +.. click:: SoftLayer.CLI.autoscale.detail:cli + :prog: autoscale detail + :show-nested: + +.. click:: SoftLayer.CLI.autoscale.scale:cli + :prog: autoscale scale + :show-nested: + +.. click:: SoftLayer.CLI.autoscale.logs:cli + :prog: autoscale logs + :show-nested: + +.. click:: SoftLayer.CLI.autoscale.tag:cli + :prog: autoscale tag + :show-nested: + +.. click:: SoftLayer.CLI.autoscale.edit:cli + :prog: autoscale edit + :show-nested: + + +.. _Autoscale Portal: https://cloud.ibm.com/classic/autoscale \ No newline at end of file diff --git a/tests/CLI/modules/autoscale_tests.py b/tests/CLI/modules/autoscale_tests.py new file mode 100644 index 000000000..9ea7ebe46 --- /dev/null +++ b/tests/CLI/modules/autoscale_tests.py @@ -0,0 +1,86 @@ +""" + SoftLayer.tests.CLI.modules.autoscale_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests for the autoscale cli command +""" +import mock + +from SoftLayer import fixtures +from SoftLayer import testing + +import tempfile + + +class AutoscaleTests(testing.TestCase): + + def test_logs_dates(self): + result = self.run_command(['autoscale', 'logs', '123456', '-d', '2019-02-02']) + print(result) + self.assert_no_fail(result) + + def test_scale_down(self): + result = self.run_command(['autoscale', 'scale', '123456', '--down', '--amount', '2']) + self.assert_no_fail(result) + + def test_scale_up(self): + result = self.run_command(['autoscale', 'scale', '123456', '--up', '--amount', '2']) + self.assert_no_fail(result) + + def test_scale_to(self): + result = self.run_command(['autoscale', 'scale', '789654123', '--down', '--amount', '2']) + self.assert_no_fail(result) + + def test_scale_by_up(self): + result = self.run_command(['autoscale', 'scale', '789654123', '--by', '--down', '--amount', '-1']) + self.assert_no_fail(result) + + def test_scale_cancel(self): + result = self.run_command(['autoscale', 'scale', '789654123', '--by', '--down', '--amount', '1']) + self.assert_no_fail(result) + + def test_autoscale_list(self): + result = self.run_command(['autoscale', 'list']) + self.assert_no_fail(result) + + def test_autoscale_detail(self): + result = self.run_command(['autoscale', 'detail', '12222222']) + self.assert_no_fail(result) + + def test_autoscale_tag(self): + result = self.run_command(['autoscale', 'tag', '12345']) + self.assert_no_fail(result) + + @mock.patch('SoftLayer.managers.autoscale.AutoScaleManager.edit') + def test_autoscale_edit(self, manager): + result = self.run_command(['autoscale', 'edit', '12345', '--name', 'test']) + self.assert_no_fail(result) + manager.assert_called_with('12345', {'name': 'test'}) + + @mock.patch('SoftLayer.managers.autoscale.AutoScaleManager.edit') + def test_autoscale_edit_userdata(self, manager): + group = fixtures.SoftLayer_Scale_Group.getObject + template = { + 'virtualGuestMemberTemplate': group['virtualGuestMemberTemplate'] + } + template['virtualGuestMemberTemplate']['userData'] = [{'value': 'test'}] + + result = self.run_command(['autoscale', 'edit', '12345', '--userdata', 'test']) + self.assert_no_fail(result) + manager.assert_called_with('12345', template) + + @mock.patch('SoftLayer.managers.autoscale.AutoScaleManager.edit') + def test_autoscale_edit_userfile(self, manager): + group = fixtures.SoftLayer_Scale_Group.getObject + template = { + 'virtualGuestMemberTemplate': group['virtualGuestMemberTemplate'] + } + template['virtualGuestMemberTemplate']['userData'] = [{'value': ''}] + + with tempfile.NamedTemporaryFile() as userfile: + result = self.run_command(['autoscale', 'edit', '12345', '--userfile', userfile.name]) + self.assert_no_fail(result) + manager.assert_called_with('12345', template) diff --git a/tests/managers/autoscale_tests.py b/tests/managers/autoscale_tests.py new file mode 100644 index 000000000..6da505409 --- /dev/null +++ b/tests/managers/autoscale_tests.py @@ -0,0 +1,125 @@ +""" + SoftLayer.tests.managers.autoscale_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" + +from SoftLayer.managers.autoscale import AutoScaleManager +from SoftLayer import testing + + +class AutoScaleTests(testing.TestCase): + + def set_up(self): + self.autoscale = AutoScaleManager(self.client) + + def test_autoscale_list(self): + self.autoscale.list() + + self.assert_called_with( + 'SoftLayer_Account', + 'getScaleGroups' + ) + + def test_autoscale_list_with_mask(self): + self.autoscale.list(mask='mask[status,virtualGuestMemberCount]') + + self.assert_called_with( + 'SoftLayer_Account', + 'getScaleGroups' + ) + + def test_autoscale_details(self): + self.autoscale.details(11111) + + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'getObject', + identifier=11111 + ) + + def test_autoscale_details_with_mask(self): + self.autoscale.details(11111, mask='mask[virtualGuestMembers[id,virtualGuest[hostname,domain,provisionDate]], ' + 'terminationPolicy,virtualGuestMemberCount]') + + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'getObject', + identifier=11111 + ) + + def test_autoscale_policy(self): + self.autoscale.get_policy(11111) + + self.assert_called_with( + 'SoftLayer_Scale_Policy', + 'getObject', + identifier=11111 + ) + + def test_autoscale_policy_with_mask(self): + self.autoscale.get_policy(11111, mask='mask[cooldown, createDate, id, name, actions, triggers[type]]') + + self.assert_called_with( + 'SoftLayer_Scale_Policy', + 'getObject', + identifier=11111 + ) + + def test_autoscale_scale(self): + self.autoscale.scale(11111, 3) + + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'scale', + identifier=11111 + ) + + def test_autoscale_scaleTo(self): + self.autoscale.scale_to(11111, 3) + + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'scaleTo', + identifier=11111 + ) + + def test_autoscale_getLogs(self): + self.autoscale.get_logs(11111) + + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'getLogs', + identifier=11111 + ) + + def test_autoscale_get_virtual_guests(self): + self.autoscale.get_virtual_guests(11111) + + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'getVirtualGuestMembers', + identifier=11111, + mask=None + ) + + def test_autoscale_get_virtual_guests_mask(self): + test_mask = "mask[id]" + self.autoscale.get_virtual_guests(11111, mask=test_mask) + + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'getVirtualGuestMembers', + identifier=11111, + mask=test_mask + ) + + def test_edit_object(self): + template = {'name': 'test'} + self.autoscale.edit(12345, template) + self.assert_called_with( + 'SoftLayer_Scale_Group', + 'editObject', + args=(template,), + identifier=12345)