Skip to content

Commit

Permalink
BN-29 Added prices, min price and ability to use spot instances to AWS
Browse files Browse the repository at this point in the history
  • Loading branch information
rabits committed Aug 29, 2020
1 parent 17783e2 commit b9044d5
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 32 deletions.
7 changes: 7 additions & 0 deletions BlendNet/Manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def __init__(self, parent, init = {}):
'type': bool,
'default': True,
}
self._defs['agent_instance_max_price'] = {
'description': '''Maximum cheap instance price to pay for the agent''',
'type': float,
'min': 0.0,
'default': None,
}
self._defs['agent_listen_host'] = {
'description': '''Agent listen host - ip address or name''',
'type': str,
Expand Down Expand Up @@ -125,6 +131,7 @@ def _agentsPoolSetup(self):
'bucket': self._cfg.bucket,
'instance_type': self._cfg.agent_instance_type,
'use_cheap_instance': self._cfg.agent_use_cheap_instance,
'instance_max_price': self._cfg.agent_instance_max_price,
'listen_host': self._cfg.agent_listen_host,
'listen_port': self._cfg.agent_listen_port,
'auth_user': self._cfg.agent_auth_user,
Expand Down
86 changes: 73 additions & 13 deletions BlendNet/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,17 +377,24 @@ def worker(callback):

def startManager(cfg = None):
cfg = cfg if cfg else getConfig()
if cfg['agent_use_cheap_instance']:
# Calculate the agent max price according the current params
price = getAgentPrice(cfg['agent_instance_type'])
cfg['agent_instance_max_price'] = price[0]
if cfg['agent_instance_max_price'] < 0.0:
print('ERROR: Unable to run Manager - unable to determine price of agent: ' + price[1])
return

if not isManagerStarted():
print('DEBUG: Running uploading to bucket')
providers.setupBucket(cfg['bucket'], cfg)

if not isManagerCreated():
print('DEBUG: Creating manager instance')
providers.createInstanceManager(cfg)
print('DEBUG: Creating the required firewall rules')
providers.createFirewall('blendnet-manager', cfg['listen_port'])
providers.createFirewall('blendnet-agent', cfg['agent_listen_port'])
print('DEBUG: Creating manager instance')
providers.createInstanceManager(cfg)
# TODO: Setup subnetwork to use internal google services
elif isManagerStopped():
print('DEBUG: Starting manager instance')
Expand Down Expand Up @@ -653,33 +660,86 @@ def getCheapMultiplierList(scene = None, context = None):
return [ (str(v),str(v),str(v)) for v in l ]


instance_type_price_manager_cache = [0.0, '']
instance_type_price_manager_cache = [(0.0, 'LOADING...'), '']

def getManagerPrice(inst_type):
global instance_type_price_manager_cache
if instance_type_price_manager_cache[1] == inst_type:
return instance_type_price_manager_cache[0]

instance_type_price_manager_cache[0] = providers.getPrice(inst_type, 1.0)
instance_type_price_manager_cache[1] = inst_type

return instance_type_price_manager_cache[0]

def getManagerPriceBG(inst_type, context = None):
def worker(callback):
getManagerPrice(inst_type)

instance_type_price_agent_cache = [0.0, '', None]
if callback:
callback()

global instance_type_price_manager_cache
info = instance_type_price_manager_cache
if info[1] != inst_type:
instance_type_price_manager_cache[1] = inst_type
callback = context.area.tag_redraw if context and context.area else None
_runBackgroundWork(worker, callback)

return info[0]


instance_type_price_agent_cache = [(0.0, 'LOADING...'), '', None, -1.0]

def getAgentPrice(inst_type):
global instance_type_price_agent_cache
prefs = bpy.context.preferences.addons[__package__.split('.', 1)[0]].preferences
if instance_type_price_agent_cache[1] == inst_type and instance_type_price_agent_cache[2] == prefs.agent_use_cheap_instance:
return instance_type_price_agent_cache[0]

cheap_multiplier = 1.0
if prefs.agent_use_cheap_instance:
prefs = bpy.context.preferences.addons[__package__.split('.', 2)[0]].preferences
if prefs.agent_use_cheap_instance and prefs.agent_cheap_multiplier != '':
cheap_multiplier = float(prefs.agent_cheap_multiplier)
instance_type_price_agent_cache[0] = providers.getPrice(inst_type, cheap_multiplier)
instance_type_price_agent_cache[1] = inst_type
instance_type_price_agent_cache[2] = prefs.agent_use_cheap_instance

return instance_type_price_agent_cache[0]

def getAgentPriceBG(inst_type, context = None):
def worker(callback):
getAgentPrice(inst_type)

if callback:
callback()

global instance_type_price_agent_cache
info = instance_type_price_agent_cache
prefs = bpy.context.preferences.addons[__package__.split('.', 1)[0]].preferences
if info[1] != inst_type or info[2] != prefs.agent_use_cheap_instance and info[3] != prefs.agent_cheap_multiplier:
instance_type_price_agent_cache[1] = inst_type
instance_type_price_agent_cache[2] = prefs.agent_use_cheap_instance
instance_type_price_agent_cache[3] = prefs.agent_cheap_multiplier
callback = context.area.tag_redraw if context and context.area else None
_runBackgroundWork(worker, callback)

return info[0]


instance_type_price_minimal_cache = [-1.0, '', 0]

def getMinimalCheapPrice(inst_type):
global instance_type_price_minimal_cache
instance_type_price_minimal_cache[0] = providers.getMinimalCheapPrice(inst_type)

return instance_type_price_minimal_cache[0]

def getMinimalCheapPriceBG(inst_type, context = None):
def worker(callback):
getMinimalCheapPrice(inst_type)

if callback:
callback()

global instance_type_price_minimal_cache
info = instance_type_price_minimal_cache
if info[1] != inst_type or time.time() > info[2]:
instance_type_price_minimal_cache[1] = inst_type
instance_type_price_minimal_cache[2] = time.time() + 600
callback = context.area.tag_redraw if context and context.area else None
_runBackgroundWork(worker, callback)

return info[0]
6 changes: 5 additions & 1 deletion BlendNet/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,8 @@ def getCheapMultiplierList():

def getPrice(inst_type, cheap_multiplier):
'''Return the price of the instance type per hour'''
return _execProviderFunc('getPrice', -1.0, inst_type, cheap_multiplier)
return _execProviderFunc('getPrice', (-1.0, 'ERR'), inst_type, cheap_multiplier)

def getMinimalCheapPrice(inst_type):
'''Finds the lowest available instance price and returns it'''
return _execProviderFunc('getMinimalCheapPrice', -1.0, inst_type)
40 changes: 33 additions & 7 deletions BlendNet/providers/aws/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def createInstanceManager(cfg):
'--instance-type', cfg['instance_type'],
'--iam-instance-profile', '{"Name":"blendnet-manager"}',
'--block-device-mappings', json.dumps(disk_config),
#'--key-name', 'default_key', # If you want to ssh to the instance (and change createFirewall func)
#'--key-name', 'default_key', # If you want to ssh to the instance (change createFirewall func too)
'--security-groups', 'blendnet-manager',
'--user-data', 'file://' + startup_script_file.name,
]
Expand Down Expand Up @@ -422,11 +422,22 @@ def createInstanceAgent(cfg):
'--instance-type', cfg['instance_type'],
'--iam-instance-profile', '{"Name":"blendnet-agent"}',
'--block-device-mappings', json.dumps(disk_config),
#'--key-name', 'default_key', # If you want to ssh to the instance (and change createFirewall func)
#'--key-name', 'default_key', # If you want to ssh to the instance (change createFirewall func too)
'--security-groups', 'blendnet-agent',
'--user-data', 'file://' + startup_script_file.name,
]

if cfg['use_cheap_instance']:
print('INFO: Running cheap agent instance with max price %f' % (cfg['instance_max_price'],))
options.append('--instance-market-options')
options.append(json.dumps({
"MarketType": "spot",
"SpotOptions": {
"MaxPrice": str(cfg['instance_max_price']),
"SpotInstanceType": "one-time",
},
}))

# TODO: make script overridable
# TODO: too much hardcode here
startup_script_file.write('''#!/bin/sh
Expand Down Expand Up @@ -534,6 +545,7 @@ def createFirewall(target_group, port):
'--description', 'Automatically created by BlendNet')
print('INFO: Creating security group for %s' % (target_group,))
# Rule to allow remote ssh to the instance
# if you enable it don't forget to remove the blendnet sec groups from AWS to recreate them
#_executeAwsTool('ec2', 'authorize-security-group-ingress',
# '--group-name', target_group,
# '--protocol', 'tcp',
Expand Down Expand Up @@ -662,7 +674,7 @@ def getAgentsNamePrefix(session_id):

def getCheapMultiplierList():
'''AWS supports spot instances which is market based on spot price'''
return [0.3] + [ i/100.0 for i in range(1, 100) ]
return [0.33] + [ i/100.0 for i in range(1, 100) ]

def getPrice(inst_type, cheap_multiplier):
'''Returns the price of the instance type per hour for the current region'''
Expand Down Expand Up @@ -692,12 +704,26 @@ def getPrice(inst_type, cheap_multiplier):
data = json.load(res)
for d in data['prices']:
if d['attributes']['aws:ec2:instanceType'] == inst_type:
# Could be USD or CNY in China
return float(list(d['price'].values())[0]) * cheap_multiplier
return -1.0
return (float(list(d['price'].values())[0]) * cheap_multiplier, list(d['price'].keys())[0])
return (-1.0, 'ERR: Unable to find the price')
except Exception as e:
print('WARN: Error during getting the instance type price:', url, e)
return -1.0
return (-1.0, 'ERR: ' + str(e))


def getMinimalCheapPrice(inst_type):
'''Will check the spot history and retreive the latest minimal price'''
data = _executeAwsTool('ec2', 'describe-spot-price-history',
'--instance-types', json.dumps([inst_type]),
'--product-descriptions', json.dumps(['Linux/UNIX']),
'--query', 'SpotPriceHistory[]')
min_prices = dict()
for it in data:
# First items in the list is latest
if it['AvailabilityZone'] not in min_prices:
min_prices[it['AvailabilityZone']] = float(it['SpotPrice'])

return min(min_prices.values())

findAWSTool()

Expand Down
8 changes: 5 additions & 3 deletions BlendNet/providers/gcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ def getPrice(inst_type, cheap_multiplier):

inst_info = _getInstanceTypeInfo(inst_type)
if not inst_info:
return -1.0
return (-1.0, 'ERR: Instance type not found')

usage_type = 'OnDemand'
if cheap_multiplier < 1.0:
Expand All @@ -784,7 +784,7 @@ def getPrice(inst_type, cheap_multiplier):
_PRICE_CACHE['data'] = []
_PRICE_CACHE['usage'] = usage_type
bill, configs = _getBilling(), _getConfigs()
#req = bill.services().list() 'businessEntityName': 'businessEntities/GCP'
#req = bill.services().list() 'businessEntityName': 'businessEntities/GCP' = 'services/6F81-5844-456A'
req = bill.services().skus().list(parent='services/6F81-5844-456A')
while req is not None:
resp = req.execute()
Expand All @@ -808,21 +808,23 @@ def getPrice(inst_type, cheap_multiplier):
_PRICE_CACHE['update'] = time.time() + 60*60

out_price = 0
out_currency = 'NON'

for it in _PRICE_CACHE['data']:
if not all([ check in it.get('description') for check in desc_check ]):
continue
exp = it.get('pricingInfo', [{}])[0].get('pricingExpression', {})
price_def = exp.get('tieredRates', [{}])[0].get('unitPrice', {})
price = float(price_def.get('unit', '0')) + price_def.get('nanos', 0)/1000000000.0
out_currency = price_def.get('currencyCode')
if ' Core ' in it.get('description'):
print('DEBUG: Price adding CPU: ' + str(price * inst_info['cpu']))
out_price += price * inst_info['cpu']
elif ' Ram ' in it.get('description'):
print('DEBUG: Price adding MEM: ' + str(price * (inst_info['mem'] / 1024.0)))
out_price += price * (inst_info['mem'] / 1024.0)

return out_price
return (out_price, out_currency)

findGoogleCloudSdk()

Expand Down
61 changes: 53 additions & 8 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,18 @@ def draw(self, context):
row.prop(self, 'blender_dist_url')
box.row().prop(self, 'blender_dist_checksum')
box_box = box.box()

box_box.label(text='Manager')
row = box_box.row()
row.prop(self, 'manager_instance_type', text='Type')
row = box_box.row()
if BlendNet.addon.getManagerPrice(self.manager_instance_type) < 0.0:
row.label(text='WARNING: Unable to find price for the type "%s"' % (self.manager_instance_type,))
price = BlendNet.addon.getManagerPriceBG(self.manager_instance_type, context)
if price[0] < 0.0:
row.label(text='WARNING: Unable to find price for the type "%s": %s' % (
self.manager_instance_type, price[1]
), icon='ERROR')
else:
row.label(text='Calculated price: ~%f/Hour (USD or region currency)' %
BlendNet.addon.getManagerPrice(self.manager_instance_type))
row.label(text='Calculated price: ~%f/Hour (%s)' % price)
row = box_box.row()
row.prop(self, 'manager_address')
row.enabled = False # TODO: remove it when functionality will be available
Expand All @@ -248,6 +251,7 @@ def draw(self, context):
row.prop(self, 'manager_user')
row = box_box.row()
row.prop(self, 'manager_password')

box_box = box.box()
box_box.label(text='Agent')
row = box_box.row()
Expand All @@ -261,11 +265,24 @@ def draw(self, context):
row.prop(self, 'manager_agent_instance_type', text='Agents type')
row.prop(self, 'manager_agents_max', text='Agents max')
row = box_box.row()
if BlendNet.addon.getAgentPrice(self.manager_agent_instance_type) < 0.0:
row.label(text='WARNING: Unable to find price for the type "%s"' % (self.manager_agent_instance_type,))
price = BlendNet.addon.getAgentPriceBG(self.manager_agent_instance_type, context)
if price[0] < 0.0:
row.label(text='ERROR: Unable to find price for the type "%s": %s' % (
self.manager_agent_instance_type, price[1]
), icon='ERROR')
else:
row.label(text='Calculated combined price: ~%f/Hour (USD or region currency)' %
(BlendNet.addon.getAgentPrice(self.manager_agent_instance_type) * self.manager_agents_max))
row.label(text='Calculated combined price: ~%f/Hour (%s)' % (
price[0] * self.manager_agents_max, price[1]
))
min_price = BlendNet.addon.getMinimalCheapPriceBG(self.manager_agent_instance_type, context)
if min_price > 0.0:
row = box_box.row()
row.label(text='Minimal combined price: ~%f/Hour' % (
min_price * self.manager_agents_max,
))
if price[0] <= min_price:
row = box_box.row()
row.label(text='ERROR: Selected cheap price is lower than minimal one', icon='ERROR')
row = box_box.row()
row.prop(self, 'agent_port')
row = box_box.row()
Expand Down Expand Up @@ -982,6 +999,14 @@ def draw(self, context):
row = layout.row()
row.enabled = not BlendNet.addon.isManagerCreated()
row.prop(prefs, 'manager_instance_type', text='Type')
price = BlendNet.addon.getManagerPriceBG(prefs.manager_instance_type, context)
row = layout.row()
if price[0] < 0.0:
row.label(text='WARNING: Unable to find price for the type "%s": %s' % (
prefs.manager_instance_type, price[1]
), icon='ERROR')
else:
row.label(text='Calculated price: ~%f/Hour (%s)' % price)

manager_info = BlendNet.addon.getResources(context).get('manager')
if manager_info:
Expand Down Expand Up @@ -1032,6 +1057,26 @@ def draw(self, context):
row.prop(prefs, 'manager_agent_instance_type', text='Agents type')
row = layout.row()
row.prop(prefs, 'manager_agents_max', text='Agents max')
row = layout.row()
price = BlendNet.addon.getAgentPriceBG(prefs.manager_agent_instance_type, context)
if price[0] < 0.0:
row.label(text='ERROR: Unable to find price for the type "%s": %s' % (
prefs.manager_agent_instance_type, price[1]
), icon='ERROR')
else:
row.label(text='Calculated combined price: ~%f/Hour (%s)' % (
price[0] * prefs.manager_agents_max, price[1]
))

min_price = BlendNet.addon.getMinimalCheapPriceBG(prefs.manager_agent_instance_type, context)
if min_price > 0.0:
row = layout.row()
row.label(text='Minimal combined price: ~%f/Hour' % (
min_price * prefs.manager_agents_max,
))
if price[0] <= min_price:
row = layout.row()
row.label(text='ERROR: Selected cheap price is lower than minimal one', icon='ERROR')

class BlendNetRenderEngine(bpy.types.RenderEngine):
'''Continuous render engine to allow switch between tasks'''
Expand Down

0 comments on commit b9044d5

Please sign in to comment.