Quotes are a way to save an order, and then easily duplicate the order later. You can create a quote from the control portal by going through the normal order process, and then instead of hitting “Order” at the end, there will be a button that says “Save Quote”, which will save the order for later. You can then use the quote service to pull down the information about it from the API. listQuotes
in the example below does exactly this.
To order from a quote you will need the following bits of information
curl -u $SL_USER:$SL_APIKEY 'https://api.softlayer.com/rest/v3.1/SoftLayer_Account/getActiveQuotes.json?objectMask=mask%5Border%5Bid%2C+orderTotalAmount%2C+orderTopLevelItems%5Bid%2C+description%5D%5D%2C+ordersFromQuoteCount%5D'
The sample code will output something like this. Quotes can be PENDING, which means they might expire if you don’t call SoftLayer_Billing_Order_Quote::saveQuote(id=quoteId) before its expiration date. SAVED quotes will stick around until you call SoftLayer_Billing_Order_Quote::deleteQuote(id=quoteId) The quoteId in the first column is what will be used for placing the order. Description is just the topLevelBillingItem for the quote.
ID, createDate, name, status, order count, description
2460465, 2018-07-23T14:20:59-06:00, Ruber, SAVED, 0, Single Intel Xeon E3-1270 v3 (4 Cores, 3.50 GHz)
2457385, 2018-07-17T14:23:54-06:00, AJCB-TESTWITHSSHKEYS, PENDING, 0, Single Intel Xeon E3-1270 v3 (4 Cores, 3.50 GHz)
1528487, 2015-09-25T15:37:27-06:00, test-quote, SAVED, 3, 1 x 2.0 GHz Core
Now we need to figure out which DC we want to place the order in. The way we are going to do this, is by checking the quote’s top level item, and see what DCs that package is available in. Using SoftLayer_Location::getDatacenters() would show you all datacenters, and not all packages are available in all datacenters.
'https://api.softlayer.com/rest/v3.1/SoftLayer_Billing_Order_Quote/<QUOTEID>/getObject.json?objectMask=mask[id,+order[id,orderTopLevelItems[id,package[id,availableLocations[location[longName,id]]]]]]'
def listLocationsForQuote(self, quoteId):
mask="mask[id, order[id, orderTopLevelItems[id, package[id, availableLocations[location[longName, id]]]]]]"
locations = self.client['Billing_Order_Quote'].getObject(id=quoteId, mask=mask)
package_id = locations['order']['orderTopLevelItems'][0]['package']['id']
print("Package {} is available in the following locations...".format(package_id))
for dc in locations['order']['orderTopLevelItems'][0]['package']['availableLocations']:
print("{}, id={}".format(dc['location']['longName'], dc['location']['id']))
Package 46 is available in the following locations...
Seattle 1, id=18171
Dallas 5, id=138124
Houston 2, id=142775
Dallas 7, id=142776
San Jose 1, id=168642
...
Ok, now we have a quote, a dc, now we just need to build our order out.
'https://api.softlayer.com/rest/v3.1/SoftLayer_Billing_Order_Quote/<QUOTEID>/getRecalculatedOrderContainer.json
This function takes a quote, and returns an order container with all the proper price ids filled in, which are required to place the actual order.
With this order container, all we need to do is fill out some extra information (basically anything that would be on the last page of the order form).
The object you send in to the placeOrder method, is going to be a SoftLayer_Container_Product_Order, or possibly one of its child classes, like Hardware_Server or Virtual_Guest
The only data you have to change here is the hostname and domain for your servers, the location, and the quantity.
If you are ordering baremetal, you need to change container[‘hardware’], if you are ordering virtual guests, you need to change container[‘virtualGuests’]
guests = {
'hostname': 'quotetest',
'domain': 'example.com'
}
quote = self.client['Billing_Order_Quote']
quote_container = quote.getRecalculatedOrderContainer(id=quote_id)
container = quote_container
container['complexType'] = "SoftLayer_Container_Product_Order_Virtual_Guest"
container['quantity'] = 2
container['virtualGuests'] = []
container['virtualGuests'].append(guests)
container['virtualGuests'].append(guests)
container['location'] = dc_id
The container['virtualGuests']
array should have 1 entry for each server you want to order. Since I am ordering 2 servers, I appeneded 2 entries to the array.
getRecalculatedOrderContainer
will return a generic Container_Product_Order type, so you will need to specify the correct type to be able to actually place an order. Orders can occasionally work without specifying this complexType, but its better to be sure. There are quite a lot of container types, the full list can be found here: https://softlayer.github.io/reference/softlayerapi/ , and search for Container_Product_Order
to see all the options.
The most common are:
To have SSH keys installed on a server at order time, we need to specify them in the [sshKeys]https://softlayer.github.io/reference/datatypes/SoftLayer_Container_Product_Order_SshKeys/) property. This property is at the container level, NOT the guest level, even though guests have a sshKey property.
This propety is an array of sshKeys, which itself is an array of ids. This means that there needs to be 1 array of keys per server you are ordering, and the order they are applied should match the order they are listed in the virtualGuests propety. sshKeys[0] => virtualGuests[0]
, sshKeys[1] => virtualGuests[1]
and so on.
An sshKey itself should look like this
sshKey = {
'sshKeyIds' : [1234, 456, 11111]
}
container['sshKeys'].append(sshKey)
container['sshKeys'].append(sshKey)
This is a bit combersome, but it does allow you the flexibility to order different sshKeys on different servers if you need to.
provisionScripts are simply an array of strings (urls) to apply to each server in order. Like sshKeys, the order these provision scripts are listed match up with their virtualGuests. Each guest will only have 1 provision script run on it.
Post provision scripts need to be HTTPS to be executed, HTTP scripts just get downloaded
scriptUrl = 'https://somesite.com/script.sh'
container['provisionScripts'] = [scriptUrl, scriptUrl]
Vlans and Subnets need to be specified at the guest level, not the container level. You will also need the VLAN/Subnet ID, which is different than the vlan number which can be a bit confusing to new api users.
Public Vlans and Public Subnets need to be added to the primaryNetworkComponent. Private Vlans and Private Subnets need to be added to the primaryBackendNetworkComponent
This example selects a specific VLAN for the primaryNetwork, and a specific Subnet for the primaryBackendNetwork. You can have both a specific vlan and specific network, assuming the subnet is already routed to the vlan you specified.
guest = {
'hostname': 'tester',
'domain': 'example.com',
'primaryNetworkComponent': {
"networkVlan": {"id": int(public_vlan)}},
"primaryBackendNetworkComponent": {
"primarySubnet": {"id": int(private_subnet)}}
}
Instead of specifying a fresh operating system, you can have a server provisioned from a imageTemplate by specifying its ID.
container['imageTemplateId'] = image_id
This ISN’T an array, so the imageTemplate here will get applied to all guests in the order.
This is the script I use when testing out bits of the ordering API, and hopefully it will be helpful in building your own way to order image templates.
import SoftLayer
from pprint import pprint as pp
class example():
def __init__(self):
self.client = SoftLayer.Client()
debugger = SoftLayer.DebugTransport(self.client.transport)
self.client.transport = debugger
def orderQuote(self, quote_id, dc_id = None, image_id = None, private_vlan = None, public_vlan = None):
# If you have more than 1 server in the quote, you will need to append
# a copy of this for each VSI, with hostnames changed as needed
guests = {
'hostname': 'quotetest',
'domain': 'example.com'
}
if public_vlan:
guests.update({
'primaryNetworkComponent': {
"networkVlan": {"id": int(public_vlan)}}})
if private_vlan:
guests.update({
"primaryBackendNetworkComponent": {
"networkVlan": {"id": int(private_vlan)}}})
quote = self.client['Billing_Order_Quote']
quote_container = quote.getRecalculatedOrderContainer(id=quote_id)
print("================= QUOTE CONTAINER =================")
pp(quote_container)
print("================= QUOTE CONTAINER =================")
container = quote_container
container['complexType'] = "SoftLayer_Container_Product_Order_Virtual_Guest"
container['quantity'] = 2
container['virtualGuests'] = []
container['virtualGuests'].append(guests)
container['virtualGuests'].append(guests)
# container['provisionScripts'] = ['https://gist.githubusercontent.com/myscript.py']
sshKeys = {'sshKeyIds': [395515, 87634]}
container['sshKeys'] = [sshKeys, sshKeys]
if image_id is not None:
container['imageTemplateId'] = image_id
if dc_id is not None:
container['location'] = dc_id
# result = self.client['Billing_Order_Quote'].verifyOrder(container, id=quote_id)
result = self.client['Billing_Order_Quote'].placeOrder(container, id=quote_id)
print("================= RECEIPT CONTAINER =================")
pp(result)
print("================= RECEIPT CONTAINER =================")
def listQuotes(self):
# https://softlayer.github.io/reference/datatypes/SoftLayer_Billing_Order_Quote/
mask = "mask[order[id, orderTotalAmount, orderTopLevelItems[id, description]], ordersFromQuoteCount]"
quotes = self.client['SoftLayer_Account'].getActiveQuotes(mask=mask)
print("ID, createDate, name, status, order count, description")
for quote in quotes:
print("{}, {}, {}, {}, {}, {}".format(
quote['id'], quote['createDate'], quote['name'], quote['status'],
quote['ordersFromQuoteCount'], quote['order']['orderTopLevelItems'][0]['description'])
)
def listLocations(self):
locations = self.client['SoftLayer_Location'].getDatacenters()
pp(locations)
def listLocationsForQuote(self, quoteId):
mask="mask[id, order[id, orderTopLevelItems[id, package[id, availableLocations[location[longName, id]]]]]]"
locations = self.client['Billing_Order_Quote'].getObject(id=quoteId, mask=mask)
package_id = locations['order']['orderTopLevelItems'][0]['package']['id']
print("Package {} is available in the following locations...".format(package_id))
for dc in locations['order']['orderTopLevelItems'][0]['package']['availableLocations']:
print("{}, id={}".format(dc['location']['longName'], dc['location']['id']))
def listSshKeys(self):
keys = self.client['SoftLayer_Account'].getSshKeys()
pp(keys)
def listImageTemplates(self):
mask = "mask[id,name,note]"
imageTemplates = self.client['SoftLayer_Account'].getPrivateBlockDeviceTemplateGroups(mask=mask)
print("ID - Name - Note")
for template in imageTemplates:
try:
print("%s - %s - %s" % (template['id'], template['name'], template['note']))
except KeyError:
print("%s - %s - %s" % (template['id'], template['name'], 'None'))
def listVlansInLocation(self, location_id):
mask = "mask[id,vlanNumber,primaryRouter[hostname,datacenter[id,name]]]"
objfilter2 = { "networkVlans":
{"primaryRouter":
{"datacenter": { "id" : {"operation":location_id} } }
}
}
subnets = self.client['SoftLayer_Account'].getNetworkVlans(mask=mask,filter=objfilter2)
for subnet in subnets:
print("%s, %s, %s" % ( subnet['id'], subnet['vlanNumber'], subnet['primaryRouter']['hostname']))
def debug(self):
for call in self.client.transport.get_last_calls():
print(self.client.transport.print_reproduceable(call))
if __name__ == "__main__":
quote_id = 1528487
main = example()
# main.listImageTemplates()
# main.listQuotes()
# main.listLocationsForQuote(quote_id)
# main.listLocations()
dal13 = 1854895
ams03 = 814994
dal09 = 449494
# main.listSshKeys()
# main.listVlansInLocation(dal13)
backend_vlan = 2068355 #951, bcr01a.dal13
front_vlan = 2068353 # 907, fcr01a.dal13
main.orderQuote(quote_id, dc_id=dal13, public_vlan=front_vlan,private_vlan=backend_vlan)
main.debug()
When placing an order from a quote, it is important to note the following.
Billing_Order_Quote
service, NOT The Product_Order
service.billingOrderItemId
is set. This will usually be returned from the Billing_Order_Quote::getRecalculatedOrderContainer() API call. But if it is removed, the order you place will not be tracked as being placed from a quote.