Skip to content

Commit 8d6e660

Browse files
authored
Merge pull request #1735 from grycap/devel
Devel
2 parents 38fb230 + 6cdf9eb commit 8d6e660

File tree

3 files changed

+86
-16
lines changed

3 files changed

+86
-16
lines changed

IM/connectors/Azure.py

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import string
2020
import base64
2121
import re
22+
import requests
2223
try:
2324
from urlparse import urlparse
2425
except ImportError:
@@ -70,10 +71,12 @@ class AzureInstanceTypeInfo:
7071
- gpu(int, optional): the number of gpus of this instance
7172
- gpu_model(str, optional): the model of the gpus of this instance
7273
- gpu_vendor(str, optional): the model of the gpus of this instance
74+
- sgx(bool, optional): True if the instance has SGX support
75+
- family(str, optional): family of the instance
7376
"""
7477

7578
def __init__(self, name="", cpu=1, mem=0, os_disk_space=0, res_disk_space=0,
76-
gpu=0, gpu_model=None, gpu_vendor=None, sgx=False):
79+
gpu=0, gpu_model=None, gpu_vendor=None, sgx=False, family=None):
7780
self.name = name
7881
self.cpu = cpu
7982
self.mem = mem
@@ -83,6 +86,8 @@ def __init__(self, name="", cpu=1, mem=0, os_disk_space=0, res_disk_space=0,
8386
self.gpu_model = gpu_model
8487
self.gpu_vendor = gpu_vendor
8588
self.sgx = sgx
89+
self.family = family
90+
self.price = None
8691

8792
def set_sgx(self):
8893
"""Guess SGX from instance name"""
@@ -126,7 +131,7 @@ def set_gpu_models(self):
126131
self.gpu_model = "Tesla M60"
127132

128133
@staticmethod
129-
def fromSKU(sku):
134+
def fromSKU(sku, prices=None):
130135
"""Get an instance type object from SKU Json data"""
131136
gpu = os_disk_space = res_disk_space = mem = cpu = 0
132137
for elem in sku.capabilities:
@@ -140,11 +145,41 @@ def fromSKU(sku):
140145
os_disk_space = int(elem.value)
141146
elif elem.name == "GPUs":
142147
gpu = int(elem.value)
143-
instance_type = AzureInstanceTypeInfo(sku.name, cpu, mem, os_disk_space, res_disk_space, gpu)
148+
instance_type = AzureInstanceTypeInfo(sku.name, cpu, mem, os_disk_space, res_disk_space, gpu, family=sku.family)
144149
instance_type.set_gpu_models()
145150
instance_type.set_sgx()
151+
if prices and sku.name in prices:
152+
instance_type.price = prices[sku.name]
146153
return instance_type
147154

155+
@staticmethod
156+
def get_price_list(region='westeurope'):
157+
"""Get the price list from Azure"""
158+
url = "https://prices.azure.com/api/retail/prices"
159+
url += "?$filter=serviceName eq 'Virtual Machines'"
160+
url += " and armRegionName eq '%s'" % region
161+
vm_prices = []
162+
163+
try:
164+
# Iterate over paginated results
165+
while url:
166+
response = requests.get(url, timeout=5)
167+
data = response.json()
168+
items = data.get("Items", [])
169+
vm_prices.extend(items)
170+
url = data.get("NextPageLink")
171+
except Exception:
172+
pass
173+
174+
# Process and print the results
175+
res = {}
176+
for item in vm_prices:
177+
sku = item.get("armSkuName")
178+
price = item.get("unitPrice")
179+
if sku and region and price:
180+
res[sku] = price
181+
return res
182+
148183

149184
class AzureCloudConnector(CloudConnector):
150185
"""
@@ -156,12 +191,14 @@ class AzureCloudConnector(CloudConnector):
156191

157192
type = "Azure"
158193
"""str with the name of the provider."""
159-
INSTANCE_TYPE = 'ExtraSmall'
194+
INSTANCE_TYPE = 'Standard_A0'
160195
"""Default instance type."""
161196
DEFAULT_LOCATION = "westeurope"
162197
"""Default location to use"""
163198
DEFAULT_USER = 'azureuser'
164199
""" default user to SSH access the VM """
200+
instance_type_list = {}
201+
""" Information about the instance types """
165202

166203
PROVISION_STATE_MAP = {
167204
'Accepted': VirtualMachine.PENDING,
@@ -244,10 +281,16 @@ def get_instance_type_by_name(self, instance_name, location, credentials, subscr
244281
def get_instance_type_list(self, credentials, subscription_id, location):
245282
compute_client = ComputeManagementClient(credentials, subscription_id)
246283

284+
if AzureCloudConnector.instance_type_list and location in AzureCloudConnector.instance_type_list:
285+
return AzureCloudConnector.instance_type_list[location]
286+
247287
try:
248288
skus = list(compute_client.resource_skus.list(filter="location eq '%s'" % location))
249-
inst_types = [AzureInstanceTypeInfo.fromSKU(sku) for sku in skus if sku.resource_type == "virtualMachines"]
250-
inst_types.sort(key=lambda x: (x.cpu, x.mem, x.gpu, x.res_disk_space))
289+
prices = AzureInstanceTypeInfo.get_price_list(location)
290+
inst_types = [AzureInstanceTypeInfo.fromSKU(sku, prices)
291+
for sku in skus if sku.resource_type == "virtualMachines"]
292+
inst_types.sort(key=lambda x: (x.price if x.price else 9999999, x.cpu, x.mem, x.gpu, x.res_disk_space))
293+
AzureCloudConnector.instance_type_list[location] = inst_types
251294
return inst_types
252295
except Exception:
253296
self.log_exception("Error getting instance type list.")
@@ -317,6 +360,9 @@ def update_system_info_from_instance(system, instance_type):
317360
conflict="other", missing="other")
318361
system.addFeature(Feature("instance_type", "=", instance_type.name),
319362
conflict="other", missing="other")
363+
if instance_type.price:
364+
system.addFeature(Feature("price", "=", instance_type.price),
365+
conflict="me", missing="other")
320366
if instance_type.gpu:
321367
system.addFeature(Feature("gpu.count", "=", instance_type.gpu),
322368
conflict="other", missing="other")
@@ -1131,6 +1177,8 @@ def alterVM(self, vm, radl, auth_data):
11311177
return (True, "")
11321178

11331179
instance_type = self.get_instance_type(new_system, credentials, subscription_id)
1180+
if not instance_type:
1181+
raise Exception("Instance type not found for the new flavor.")
11341182
vm_parameters = " { 'hardware_profile': { 'vm_size': %s } } " % instance_type.name
11351183

11361184
async_vm_update = compute_client.virtual_machines.begin_create_or_update(group_name,
@@ -1285,7 +1333,6 @@ def list_images(self, auth_data, filters=None):
12851333

12861334
def get_quotas(self, auth_data, region=None):
12871335
credentials, subscription_id = self.get_credentials(auth_data)
1288-
compute_client = ComputeManagementClient(credentials, subscription_id)
12891336
location = self.DEFAULT_LOCATION
12901337
try:
12911338
# Get the region from the auth data
@@ -1294,8 +1341,10 @@ def get_quotas(self, auth_data, region=None):
12941341
pass
12951342
if region:
12961343
location = region
1344+
return self._get_quotas(credentials, subscription_id, location)
12971345

1298-
# Initialize default values
1346+
def _get_quotas(self, credentials, subscription_id, location):
1347+
compute_client = ComputeManagementClient(credentials, subscription_id)
12991348
quotas = {}
13001349

13011350
try:
@@ -1315,7 +1364,7 @@ def get_quotas(self, auth_data, region=None):
13151364
quotas["volumes"]["used"] = usage.current_value
13161365
quotas["volumes"]["limit"] = usage.limit
13171366
elif "family vcpus" in name:
1318-
fam = usage.name.localized_value[:-13].strip().replace(" ", "_")
1367+
fam = usage.name.value
13191368
quotas[fam] = {"cores": {}}
13201369
quotas[fam]["cores"]["used"] = usage.current_value
13211370
quotas[fam]["cores"]["limit"] = usage.limit

IM/tosca/Tosca.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def verify_fake(tpl):
5757
self._gen_random_input_values()
5858
self.tosca = ToscaTemplate(yaml_dict_tpl=copy.deepcopy(self.yaml))
5959
except Exception as ex:
60+
Tosca.logger.exception("Error parsing TOSCA template")
6061
raise Exception("Error parsing TOSCA template: %s" % str(ex))
6162

6263
def _gen_random_input_values(self):

test/unit/connectors/Azure.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def test_10_concrete(self, credentials, compute_client):
9393
client = MagicMock()
9494
compute_client.return_value = client
9595
client.resource_skus.list.return_value = [sku]
96+
AzureCloudConnector.instance_type_list = {}
9697

9798
concrete = azure_cloud.concreteSystem(radl_system, auth)
9899
self.assertEqual(len(concrete), 1)
@@ -143,7 +144,8 @@ def create_vm(self, group_name, vm_name, vm_parameters):
143144
@patch('IM.connectors.Azure.NetworkManagementClient')
144145
@patch('IM.connectors.Azure.ClientSecretCredential')
145146
@patch('IM.InfrastructureList.InfrastructureList.save_data')
146-
def test_20_launch(self, save_data, credentials, network_client, compute_client, resource_client):
147+
@patch('requests.get')
148+
def test_20_launch(self, get_mock, save_data, credentials, network_client, compute_client, resource_client):
147149
radl_data = """
148150
network net1 (outbound = 'yes' and outports = '8080,9000:9100' and sg_name = 'nsgname')
149151
network net2 ()
@@ -222,6 +224,11 @@ def test_20_launch(self, save_data, credentials, network_client, compute_client,
222224
os_cap.value = "102400"
223225
sku.capabilities = [cpu_cap, mem_cap, res_cap, os_cap]
224226
cclient.resource_skus.list.return_value = [sku]
227+
AzureCloudConnector.instance_type_list = {}
228+
229+
prices = MagicMock()
230+
prices.json.return_value = {'Items': [{'armSkuName': 'Standard_A1', 'unitPrice': 0.1}]}
231+
get_mock.return_value = prices
225232

226233
cclient.virtual_machines.begin_create_or_update.side_effect = self.create_vm
227234

@@ -344,7 +351,8 @@ def test_20_launch(self, save_data, credentials, network_client, compute_client,
344351
@patch('IM.connectors.Azure.ComputeManagementClient')
345352
@patch('IM.connectors.Azure.DnsManagementClient')
346353
@patch('IM.connectors.Azure.ClientSecretCredential')
347-
def test_30_updateVMInfo(self, credentials, dns_client, compute_client, network_client):
354+
@patch('requests.get')
355+
def test_30_updateVMInfo(self, get_mock, credentials, dns_client, compute_client, network_client):
348356
radl_data = """
349357
network net (outbound = 'yes')
350358
system test (
@@ -388,6 +396,11 @@ def test_30_updateVMInfo(self, credentials, dns_client, compute_client, network_
388396
cclient = MagicMock()
389397
compute_client.return_value = cclient
390398
cclient.resource_skus.list.return_value = [sku]
399+
AzureCloudConnector.instance_type_list = {}
400+
401+
prices = MagicMock()
402+
prices.json.return_value = {'Items': [{'armSkuName': 'Standard_A1', 'unitPrice': 0.1}]}
403+
get_mock.return_value = prices
391404

392405
avm = MagicMock()
393406
avm.provisioning_state = "Succeeded"
@@ -493,7 +506,8 @@ def test_52_reboot(self, credentials, compute_client):
493506
@patch('IM.connectors.Azure.ComputeManagementClient')
494507
@patch('IM.connectors.Azure.NetworkManagementClient')
495508
@patch('IM.connectors.Azure.ClientSecretCredential')
496-
def test_55_alter(self, credentials, network_client, compute_client, resource_client):
509+
@patch('requests.get')
510+
def test_55_alter(self, get_mock, credentials, network_client, compute_client, resource_client):
497511
radl_data = """
498512
network net (outbound = 'yes')
499513
system test (
@@ -539,6 +553,11 @@ def test_55_alter(self, credentials, network_client, compute_client, resource_cl
539553
cclient = MagicMock()
540554
compute_client.return_value = cclient
541555
cclient.resource_skus.list.return_value = [sku]
556+
AzureCloudConnector.instance_type_list = {}
557+
558+
prices = MagicMock()
559+
prices.json.return_value = {'Items': [{'armSkuName': 'Standard_A2', 'unitPrice': 0.1}]}
560+
get_mock.return_value = prices
542561

543562
vm = MagicMock()
544563
vm.provisioning_state = "Succeeded"
@@ -568,6 +587,7 @@ def test_55_alter(self, credentials, network_client, compute_client, resource_cl
568587

569588
success, _ = azure_cloud.alterVM(vm, new_radl, auth)
570589

590+
print(self.log.getvalue())
571591
self.assertTrue(success, msg="ERROR: modifying VM info.")
572592
self.assertNotIn("ERROR", self.log.getvalue(), msg="ERROR found in log: %s" % self.log.getvalue())
573593

@@ -709,8 +729,8 @@ def test_get_quotas(self, credentials, compute_client):
709729
cclient = MagicMock()
710730
compute_client.return_value = cclient
711731
name1 = MagicMock(localized_value='Total Regional vCPUs')
712-
name2 = MagicMock(localized_value='Standard DSv3 Family vCPUs')
713-
name3 = MagicMock(localized_value='Standard Dv3 Family vCPUs')
732+
name2 = MagicMock(localized_value='Standard DSv3 Family vCPUs', value='standardDSv3Family')
733+
name3 = MagicMock(localized_value='Standard Dv3 Family vCPUs', value='standardDv3Family')
714734
name4 = MagicMock(localized_value='Standard Storage Managed Disks')
715735
name5 = MagicMock(localized_value='Virtual Machines')
716736
elem1 = MagicMock(limit=10, current_value=5)
@@ -729,8 +749,8 @@ def test_get_quotas(self, credentials, compute_client):
729749

730750
expected_quotas = {
731751
'cores': {'used': 5, 'limit': 10},
732-
'Standard_DSv3': {'cores': {'used': 4, 'limit': 8}},
733-
'Standard_Dv3': {'cores': {'used': 2, 'limit': 6}},
752+
'standardDSv3Family': {'cores': {'used': 4, 'limit': 8}},
753+
'standardDv3Family': {'cores': {'used': 2, 'limit': 6}},
734754
'volumes': {'used': 10, 'limit': 20},
735755
'instances': {'used': 1, 'limit': 20}
736756
}

0 commit comments

Comments
 (0)