Skip to content

Commit e89c8f9

Browse files
Merge pull request #1051 from allmightyspiff/1026
Reserved Capacity Support
2 parents 3a50f46 + 4cff83c commit e89c8f9

33 files changed

+1339
-337
lines changed

SoftLayer/CLI/columns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def mask(self):
5757
def get_formatter(columns):
5858
"""This function returns a callback to use with click options.
5959
60-
The retuend function parses a comma-separated value and returns a new
60+
The returned function parses a comma-separated value and returns a new
6161
ColumnFormatter.
6262
6363
:param columns: a list of Column instances

SoftLayer/CLI/routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
('virtual:reload', 'SoftLayer.CLI.virt.reload:cli'),
3131
('virtual:upgrade', 'SoftLayer.CLI.virt.upgrade:cli'),
3232
('virtual:credentials', 'SoftLayer.CLI.virt.credentials:cli'),
33+
('virtual:capacity', 'SoftLayer.CLI.virt.capacity:cli'),
3334

3435
('dedicatedhost', 'SoftLayer.CLI.dedicatedhost'),
3536
('dedicatedhost:list', 'SoftLayer.CLI.dedicatedhost.list:cli'),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Manages Reserved Capacity."""
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import importlib
5+
import os
6+
7+
import click
8+
9+
CONTEXT = {'help_option_names': ['-h', '--help'],
10+
'max_content_width': 999}
11+
12+
13+
class CapacityCommands(click.MultiCommand):
14+
"""Loads module for capacity related commands.
15+
16+
Will automatically replace _ with - where appropriate.
17+
I'm not sure if this is better or worse than using a long list of manual routes, so I'm trying it here.
18+
CLI/virt/capacity/create_guest.py -> slcli vs capacity create-guest
19+
"""
20+
21+
def __init__(self, **attrs):
22+
click.MultiCommand.__init__(self, **attrs)
23+
self.path = os.path.dirname(__file__)
24+
25+
def list_commands(self, ctx):
26+
"""List all sub-commands."""
27+
commands = []
28+
for filename in os.listdir(self.path):
29+
if filename == '__init__.py':
30+
continue
31+
if filename.endswith('.py'):
32+
commands.append(filename[:-3].replace("_", "-"))
33+
commands.sort()
34+
return commands
35+
36+
def get_command(self, ctx, cmd_name):
37+
"""Get command for click."""
38+
path = "%s.%s" % (__name__, cmd_name)
39+
path = path.replace("-", "_")
40+
module = importlib.import_module(path)
41+
return getattr(module, 'cli')
42+
43+
44+
# Required to get the sub-sub-sub command to work.
45+
@click.group(cls=CapacityCommands, context_settings=CONTEXT)
46+
def cli():
47+
"""Base command for all capacity related concerns"""
48+
pass
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Create a Reserved Capacity instance."""
2+
3+
import click
4+
5+
6+
from SoftLayer.CLI import environment
7+
from SoftLayer.CLI import formatting
8+
from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager
9+
10+
11+
@click.command(epilog=click.style("""WARNING: Reserved Capacity is on a yearly contract"""
12+
""" and not cancelable until the contract is expired.""", fg='red'))
13+
@click.option('--name', '-n', required=True, prompt=True,
14+
help="Name for your new reserved capacity")
15+
@click.option('--backend_router_id', '-b', required=True, prompt=True, type=int,
16+
help="backendRouterId, create-options has a list of valid ids to use.")
17+
@click.option('--flavor', '-f', required=True, prompt=True,
18+
help="Capacity keyname (C1_2X2_1_YEAR_TERM for example).")
19+
@click.option('--instances', '-i', required=True, prompt=True, type=int,
20+
help="Number of VSI instances this capacity reservation can support.")
21+
@click.option('--test', is_flag=True,
22+
help="Do not actually create the virtual server")
23+
@environment.pass_env
24+
def cli(env, name, backend_router_id, flavor, instances, test=False):
25+
"""Create a Reserved Capacity instance.
26+
27+
*WARNING*: Reserved Capacity is on a yearly contract and not cancelable until the contract is expired.
28+
"""
29+
manager = CapacityManager(env.client)
30+
31+
result = manager.create(
32+
name=name,
33+
backend_router_id=backend_router_id,
34+
flavor=flavor,
35+
instances=instances,
36+
test=test)
37+
if test:
38+
table = formatting.Table(['Name', 'Value'], "Test Order")
39+
container = result['orderContainers'][0]
40+
table.add_row(['Name', container['name']])
41+
table.add_row(['Location', container['locationObject']['longName']])
42+
for price in container['prices']:
43+
table.add_row(['Contract', price['item']['description']])
44+
table.add_row(['Hourly Total', result['postTaxRecurring']])
45+
else:
46+
table = formatting.Table(['Name', 'Value'], "Reciept")
47+
table.add_row(['Order Date', result['orderDate']])
48+
table.add_row(['Order ID', result['orderId']])
49+
table.add_row(['status', result['placedOrder']['status']])
50+
table.add_row(['Hourly Total', result['orderDetails']['postTaxRecurring']])
51+
env.fout(table)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""List Reserved Capacity"""
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import click
5+
6+
from SoftLayer.CLI import environment
7+
from SoftLayer.CLI import formatting
8+
from SoftLayer.CLI import helpers
9+
from SoftLayer.CLI.virt.create import _parse_create_args as _parse_create_args
10+
from SoftLayer.CLI.virt.create import _update_with_like_args as _update_with_like_args
11+
from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager
12+
13+
14+
@click.command()
15+
@click.option('--capacity-id', type=click.INT, help="Reserve capacity Id to provision this guest into.")
16+
@click.option('--primary-disk', type=click.Choice(['25', '100']), default='25', help="Size of the main drive.")
17+
@click.option('--hostname', '-H', required=True, prompt=True, help="Host portion of the FQDN.")
18+
@click.option('--domain', '-D', required=True, prompt=True, help="Domain portion of the FQDN.")
19+
@click.option('--os', '-o', help="OS install code. Tip: you can specify <OS>_LATEST.")
20+
@click.option('--image', help="Image ID. See: 'slcli image list' for reference.")
21+
@click.option('--boot-mode', type=click.STRING,
22+
help="Specify the mode to boot the OS in. Supported modes are HVM and PV.")
23+
@click.option('--postinstall', '-i', help="Post-install script to download.")
24+
@helpers.multi_option('--key', '-k', help="SSH keys to add to the root user.")
25+
@helpers.multi_option('--disk', help="Additional disk sizes.")
26+
@click.option('--private', is_flag=True, help="Forces the VS to only have access the private network.")
27+
@click.option('--like', is_eager=True, callback=_update_with_like_args,
28+
help="Use the configuration from an existing VS.")
29+
@click.option('--network', '-n', help="Network port speed in Mbps.")
30+
@helpers.multi_option('--tag', '-g', help="Tags to add to the instance.")
31+
@click.option('--userdata', '-u', help="User defined metadata string.")
32+
@click.option('--ipv6', is_flag=True, help="Adds an IPv6 address to this guest")
33+
@click.option('--test', is_flag=True,
34+
help="Test order, will return the order container, but not actually order a server.")
35+
@environment.pass_env
36+
def cli(env, **args):
37+
"""Allows for creating a virtual guest in a reserved capacity."""
38+
create_args = _parse_create_args(env.client, args)
39+
if args.get('ipv6'):
40+
create_args['ipv6'] = True
41+
create_args['primary_disk'] = args.get('primary_disk')
42+
manager = CapacityManager(env.client)
43+
capacity_id = args.get('capacity_id')
44+
test = args.get('test')
45+
46+
result = manager.create_guest(capacity_id, test, create_args)
47+
48+
env.fout(_build_receipt(result, test))
49+
50+
51+
def _build_receipt(result, test=False):
52+
title = "OrderId: %s" % (result.get('orderId', 'No order placed'))
53+
table = formatting.Table(['Item Id', 'Description'], title=title)
54+
table.align['Description'] = 'l'
55+
56+
if test:
57+
prices = result['prices']
58+
else:
59+
prices = result['orderDetails']['prices']
60+
61+
for item in prices:
62+
table.add_row([item['id'], item['item']['description']])
63+
return table
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""List options for creating Reserved Capacity"""
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import click
5+
6+
from SoftLayer.CLI import environment
7+
from SoftLayer.CLI import formatting
8+
from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager
9+
10+
11+
@click.command()
12+
@environment.pass_env
13+
def cli(env):
14+
"""List options for creating Reserved Capacity"""
15+
manager = CapacityManager(env.client)
16+
items = manager.get_create_options()
17+
18+
items.sort(key=lambda term: int(term['capacity']))
19+
table = formatting.Table(["KeyName", "Description", "Term", "Default Hourly Price Per Instance"],
20+
title="Reserved Capacity Options")
21+
table.align["Hourly Price"] = "l"
22+
table.align["Description"] = "l"
23+
table.align["KeyName"] = "l"
24+
for item in items:
25+
table.add_row([
26+
item['keyName'], item['description'], item['capacity'], get_price(item)
27+
])
28+
env.fout(table)
29+
30+
regions = manager.get_available_routers()
31+
location_table = formatting.Table(['Location', 'POD', 'BackendRouterId'], 'Orderable Locations')
32+
for region in regions:
33+
for location in region['locations']:
34+
for pod in location['location']['pods']:
35+
location_table.add_row([region['keyname'], pod['backendRouterName'], pod['backendRouterId']])
36+
env.fout(location_table)
37+
38+
39+
def get_price(item):
40+
"""Finds the price with the default locationGroupId"""
41+
the_price = "No Default Pricing"
42+
for price in item.get('prices', []):
43+
if not price.get('locationGroupId'):
44+
the_price = "%0.4f" % float(price['hourlyRecurringFee'])
45+
return the_price
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Shows the details of a reserved capacity group"""
2+
3+
import click
4+
5+
from SoftLayer.CLI import columns as column_helper
6+
from SoftLayer.CLI import environment
7+
from SoftLayer.CLI import formatting
8+
from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager
9+
10+
COLUMNS = [
11+
column_helper.Column('Id', ('id',)),
12+
column_helper.Column('hostname', ('hostname',)),
13+
column_helper.Column('domain', ('domain',)),
14+
column_helper.Column('primary_ip', ('primaryIpAddress',)),
15+
column_helper.Column('backend_ip', ('primaryBackendIpAddress',)),
16+
]
17+
18+
DEFAULT_COLUMNS = [
19+
'id',
20+
'hostname',
21+
'domain',
22+
'primary_ip',
23+
'backend_ip'
24+
]
25+
26+
27+
@click.command(epilog="Once provisioned, virtual guests can be managed with the slcli vs commands")
28+
@click.argument('identifier')
29+
@click.option('--columns',
30+
callback=column_helper.get_formatter(COLUMNS),
31+
help='Columns to display. [options: %s]'
32+
% ', '.join(column.name for column in COLUMNS),
33+
default=','.join(DEFAULT_COLUMNS),
34+
show_default=True)
35+
@environment.pass_env
36+
def cli(env, identifier, columns):
37+
"""Reserved Capacity Group details. Will show which guests are assigned to a reservation."""
38+
39+
manager = CapacityManager(env.client)
40+
mask = """mask[instances[id,createDate,guestId,billingItem[id, description, recurringFee, category[name]],
41+
guest[modifyDate,id, primaryBackendIpAddress, primaryIpAddress,domain, hostname]]]"""
42+
result = manager.get_object(identifier, mask)
43+
44+
try:
45+
flavor = result['instances'][0]['billingItem']['description']
46+
except KeyError:
47+
flavor = "Pending Approval..."
48+
49+
table = formatting.Table(columns.columns, title="%s - %s" % (result.get('name'), flavor))
50+
# RCI = Reserved Capacity Instance
51+
for rci in result['instances']:
52+
guest = rci.get('guest', None)
53+
if guest is not None:
54+
table.add_row([value or formatting.blank() for value in columns.row(guest)])
55+
else:
56+
table.add_row(['-' for value in columns.columns])
57+
env.fout(table)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""List Reserved Capacity"""
2+
3+
import click
4+
5+
from SoftLayer.CLI import environment
6+
from SoftLayer.CLI import formatting
7+
from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager
8+
9+
10+
@click.command()
11+
@environment.pass_env
12+
def cli(env):
13+
"""List Reserved Capacity groups."""
14+
manager = CapacityManager(env.client)
15+
result = manager.list()
16+
table = formatting.Table(
17+
["ID", "Name", "Capacity", "Flavor", "Location", "Created"],
18+
title="Reserved Capacity"
19+
)
20+
for r_c in result:
21+
occupied_string = "#" * int(r_c.get('occupiedInstanceCount', 0))
22+
available_string = "-" * int(r_c.get('availableInstanceCount', 0))
23+
24+
try:
25+
flavor = r_c['instances'][0]['billingItem']['description']
26+
# cost = float(r_c['instances'][0]['billingItem']['hourlyRecurringFee'])
27+
except KeyError:
28+
flavor = "Unknown Billing Item"
29+
location = r_c['backendRouter']['hostname']
30+
capacity = "%s%s" % (occupied_string, available_string)
31+
table.add_row([r_c['id'], r_c['name'], capacity, flavor, location, r_c['createDate']])
32+
env.fout(table)

SoftLayer/CLI/virt/create.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ def _parse_create_args(client, args):
7272
:param dict args: CLI arguments
7373
"""
7474
data = {
75-
"hourly": args['billing'] == 'hourly',
75+
"hourly": args.get('billing', 'hourly') == 'hourly',
7676
"domain": args['domain'],
7777
"hostname": args['hostname'],
78-
"private": args['private'],
79-
"dedicated": args['dedicated'],
78+
"private": args.get('private', None),
79+
"dedicated": args.get('dedicated', None),
8080
"disks": args['disk'],
8181
"cpus": args.get('cpu', None),
8282
"memory": args.get('memory', None),
@@ -89,7 +89,7 @@ def _parse_create_args(client, args):
8989
if not args.get('san') and args.get('flavor'):
9090
data['local_disk'] = None
9191
else:
92-
data['local_disk'] = not args['san']
92+
data['local_disk'] = not args.get('san')
9393

9494
if args.get('os'):
9595
data['os_code'] = args['os']

0 commit comments

Comments
 (0)