aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShankaranarayanan Puzhavakath Narayanan <snarayanan@research.att.com>2019-04-20 17:03:04 +0000
committerGerrit Code Review <gerrit@onap.org>2019-04-20 17:03:04 +0000
commit9eadcba0fa858a031f6269fc127c9dc738bb7d8b (patch)
tree88b0226afa174c77c59cbfda35ca092774518f6e
parente1f6d80752920a7ef990134f02abb3db9b5a6232 (diff)
parentfc3ead31e631f69fabf0baaa20c10bf955ce374b (diff)
Merge "Traffic Distributtion support added"
-rw-r--r--config/common_config.yaml8
-rw-r--r--config/has_config.yaml37
-rw-r--r--osdf/adapters/local_data/local_policies.py3
-rw-r--r--osdf/adapters/policy/interface.py4
-rw-r--r--osdf/models/api/placementRequest.py3
-rw-r--r--osdf/optimizers/placementopt/conductor/api_builder.py32
-rw-r--r--osdf/optimizers/placementopt/conductor/translation.py27
-rwxr-xr-xosdf/templates/conductor_interface.json80
-rw-r--r--test/conductor/test_conductor_calls.py5
-rw-r--r--test/conductor/test_conductor_translation.py18
-rw-r--r--test/placement-tests/request_placement_vfmod.json88
-rw-r--r--test/placement-tests/request_vfmod.json58
-rw-r--r--test/placement-tests/response_vfmod.json229
-rw-r--r--test/policy-local-files/QueryPolicy_vFW_TD.json30
-rw-r--r--test/policy-local-files/affinity_vFW_TD.json28
-rw-r--r--test/policy-local-files/meta-valid-policies.txt4
-rw-r--r--test/policy-local-files/vnfPolicy_vFW_TD.json35
-rw-r--r--test/policy-local-files/vnfPolicy_vPGN_TD.json35
-rw-r--r--test/test_ConductorApiBuilder.py24
-rw-r--r--test/test_api_validation.py10
-rw-r--r--test/test_get_opt_query_data.py92
-rw-r--r--test/test_process_placement_opt.py14
22 files changed, 744 insertions, 120 deletions
diff --git a/config/common_config.yaml b/config/common_config.yaml
index c513d5e..c786d74 100644
--- a/config/common_config.yaml
+++ b/config/common_config.yaml
@@ -32,6 +32,12 @@ osdf_temp: # special configuration required for "workarounds" or testing
- Placement_Optimization_1.json
- QueryPolicy_vFW.json
- vnfPolicy_vFW.json
+ placement_policy_dir_vfw_td: "./test/policy-local-files/"
+ placement_policy_files_vfw_td:
+ - vnfPolicy_vFW_TD.json
+ - vnfPolicy_vPGN_TD.json
+ - affinity_vFW_TD.json
+ - QueryPolicy_vFW_TD.json
service_info:
vCPE:
vcpeHostName: requestParameters.vcpeHostName
@@ -63,7 +69,7 @@ policy_info:
policy_scope:
default_scope: OSDF_CASABLANCA
vcpe_scope: OSDF_CASABLANCA
- vfw_scope: OSDF_CASABLANCA
+ vfw_scope: OSDF_DUBLIN
secondary_scopes:
-
- get_param: service_name
diff --git a/config/has_config.yaml b/config/has_config.yaml
index cf8a80c..38a4781 100644
--- a/config/has_config.yaml
+++ b/config/has_config.yaml
@@ -1,24 +1,27 @@
policy_config_mapping:
attributes:
- hypervisor: hypervisor,
- cloud_version: cloudVersion,
- cloud_type: cloudType,
- dataplane: dataPlane,
- network_roles: networkRoles,
- complex: complex,
- state: state,
- country: country,
- geo_region: geoRegion,
- exclusivity_groups: exclusivityGroups,
- replication_role: replicationRole,
- customer-id: customerId,
- service-type: serviceResourceId,
- equipment-role: equipmentRole,
- model-invariant-id: modelInvariantId,
- model-version-id: modelVersionId
+ hypervisor: hypervisor
+ cloudVersion: cloud_version
+ cloudType: cloud_type
+ dataPlane: dataplane
+ networkRoles: network_roles
+ complex: complex
+ state: state
+ country: country
+ geoRegion: geo_region
+ exclusivityGroups: exclusivity_groups
+ replicationRole: replication_role
+ customerId: customer_id
+ serviceResourceId: service-type
+ equipmentRole: equipment-role
+ modelInvariantId: model-invariant-id
+ modelVersionId: model-version-id
+ cloudRegionId: cloud-region-id
+ orchestrationStatus: orchestration-status
+ provStatus: prov-status
candidates:
# for (k1, v1), if k1 is in demand, set prop[k2] = _get_candidates(demand[k1])
- excludedCandidates: excluded_candidates,
+ excludedCandidates: excluded_candidates
requiredCandidates: required_candidates
extra_fields:
# we have [k1, k2, k3, k4] type items and x is policy-content-properties
diff --git a/osdf/adapters/local_data/local_policies.py b/osdf/adapters/local_data/local_policies.py
index 6e49388..dc6837a 100644
--- a/osdf/adapters/local_data/local_policies.py
+++ b/osdf/adapters/local_data/local_policies.py
@@ -19,7 +19,7 @@
import json
import os
import re
-
+from osdf.logging.osdf_logging import debug_log
def get_local_policies(local_policy_folder, local_policy_list, policy_id_list=None):
"""
@@ -32,6 +32,7 @@ def get_local_policies(local_policy_folder, local_policy_list, policy_id_list=No
:param policy_id_list: list of policies to get (if unspecified or None, get all)
:return: get policies
"""
+ debug_log.debug("Policy folder: {}, local_list {}, policy id list {}".format(local_policy_folder, local_policy_list, policy_id_list))
policies = []
if policy_id_list:
for policy_id in policy_id_list:
diff --git a/osdf/adapters/policy/interface.py b/osdf/adapters/policy/interface.py
index 7de5858..0f20667 100644
--- a/osdf/adapters/policy/interface.py
+++ b/osdf/adapters/policy/interface.py
@@ -160,10 +160,12 @@ def local_policies_location(req_json, osdf_config, service_type):
if lp.get('global_disabled'):
return None # short-circuit to disable all local policies
if lp.get('local_{}_policies_enabled'.format(service_type)):
+ debug_log.debug('Loading local policies for service type: {}'.format(service_type))
if service_type == "scheduling":
return lp.get('{}_policy_dir'.format(service_type)), lp.get('{}_policy_files'.format(service_type))
else:
service_name = req_json['serviceInfo']['serviceName'] # TODO: data_mapping.get_service_type(model_name)
+ debug_log.debug('Loading local policies for service name: {}'.format(service_name))
return lp.get('{}_policy_dir_{}'.format(service_type, service_name.lower())), \
lp.get('{}_policy_files_{}'.format(service_type, service_name.lower()))
return None
@@ -181,6 +183,8 @@ def get_policies(request_json, service_type):
local_info = local_policies_location(request_json, osdf_config, service_type)
if local_info: # tuple containing location and list of files
+ if local_info[0] is None or local_info[1] is None:
+ raise ValueError("Error fetching local policy info")
to_filter = None
if osdf_config.core['policy_info'][service_type]['policy_fetch'] == "by_name":
to_filter = request_json[service_type + "Info"]['policyId']
diff --git a/osdf/models/api/placementRequest.py b/osdf/models/api/placementRequest.py
index 55f0a98..7d6bde4 100644
--- a/osdf/models/api/placementRequest.py
+++ b/osdf/models/api/placementRequest.py
@@ -17,7 +17,7 @@
#
from .common import OSDFModel
-from schematics.types import BaseType, StringType, URLType, IntType
+from schematics.types import BaseType, StringType, URLType, IntType, BooleanType
from schematics.types.compound import ModelType, ListType, DictType
@@ -71,6 +71,7 @@ class PlacementDemand(OSDFModel):
resourceModuleName = StringType(required=True)
serviceResourceId = StringType(required=True)
tenantId = StringType()
+ unique = BooleanType() # to be implemented on the policy level
resourceModelInfo = ModelType(ModelMetaData, required=True)
existingCandidates = ListType(ModelType(Candidates))
excludedCandidates = ListType(ModelType(Candidates))
diff --git a/osdf/optimizers/placementopt/conductor/api_builder.py b/osdf/optimizers/placementopt/conductor/api_builder.py
index 187f9f5..08a7460 100644
--- a/osdf/optimizers/placementopt/conductor/api_builder.py
+++ b/osdf/optimizers/placementopt/conductor/api_builder.py
@@ -25,6 +25,29 @@ from osdf.adapters.policy.utils import group_policies_gen
from osdf.utils.programming_utils import list_flatten
+def _build_parameters(group_policies, request_json):
+ """
+ Function prepares parameters section for has request
+ :param group_policies: filtered policies
+ :param request_json: parameter data received from a client
+ :return:
+ """
+ initial_params = tr.get_opt_query_data(request_json, group_policies['request_param_query'])
+ params = dict()
+ params.update({"REQUIRED_MEM": initial_params.pop("requiredMemory", "")})
+ params.update({"REQUIRED_DISK": initial_params.pop("requiredDisk", "")})
+ params.update({"customer_lat": initial_params.pop("customerLatitude", 0.0)})
+ params.update({"customer_long": initial_params.pop("customerLongitude", 0.0)})
+ params.update({"service_name": request_json['serviceInfo']['serviceName']})
+ params.update({"service_id": request_json['serviceInfo']['serviceInstanceId']})
+
+ for key, val in initial_params.items():
+ if val and val != "":
+ params.update({key: val})
+
+ return params
+
+
def conductor_api_builder(request_json, flat_policies: list, local_config,
template="osdf/templates/conductor_interface.json"):
"""Build an OSDF southbound API call for HAS-Conductor/Placement optimization
@@ -54,7 +77,7 @@ def conductor_api_builder(request_json, flat_policies: list, local_config,
reservation_policy_list = tr.gen_reservation_policy(demand_vnf_name_list, gp['instance_reservation'])
capacity_policy_list = tr.gen_capacity_policy(demand_vnf_name_list, gp['vim_fit'])
hpa_policy_list = tr.gen_hpa_policy(demand_vnf_name_list, gp['hpa'])
- req_params_dict = tr.get_opt_query_data(request_json, gp['request_param_query'])
+ req_params_dict = _build_parameters(gp, request_json)
conductor_policies = [attribute_policy_list, distance_to_location_policy_list, inventory_policy_list,
resource_instance_policy_list, resource_region_policy_list, zone_policy_list,
reservation_policy_list, capacity_policy_list, hpa_policy_list]
@@ -70,12 +93,7 @@ def conductor_api_builder(request_json, flat_policies: list, local_config,
name=req_info['requestId'],
timeout=req_info['timeout'],
limit=req_info['numSolutions'],
- service_type=request_json['serviceInfo']['serviceName'],
- service_id=request_json['serviceInfo']['serviceInstanceId'],
- latitude=req_params_dict.get("customerLatitude", 0.0),
- longitude=req_params_dict.get("customerLongitude", 0.0),
- required_disk=req_params_dict.get("requiredDisk", ""),
- required_mem=req_params_dict.get("requiredMemory", ""),
+ request_params=req_params_dict,
json=json)
json_payload = json.dumps(json.loads(rendered_req)) # need this because template's JSON is ugly!
return json_payload
diff --git a/osdf/optimizers/placementopt/conductor/translation.py b/osdf/optimizers/placementopt/conductor/translation.py
index 93b80bf..d14f3e1 100644
--- a/osdf/optimizers/placementopt/conductor/translation.py
+++ b/osdf/optimizers/placementopt/conductor/translation.py
@@ -18,6 +18,7 @@
import copy
import json
import yaml
+import re
from osdf.utils.data_conversion import text_to_symbol
from osdf.utils.programming_utils import dot_notation
@@ -227,6 +228,8 @@ def get_demand_properties(demand, policies):
inventory_type=policy_property['inventoryType'],
service_type=demand['serviceResourceId'],
service_resource_id=demand['serviceResourceId'])
+
+ prop.update({'unique': demand['unique']} if demand.get('unique') else {})
prop['attributes'] = dict()
prop['attributes'].update({'global-customer-id': policy_property['customerId']}
if policy_property['customerId'] else {})
@@ -236,11 +239,35 @@ def get_demand_properties(demand, policies):
if demand['resourceModelInfo']['modelVersionId'] else {})
prop['attributes'].update({'equipment-role': policy_property['equipmentRole']}
if policy_property['equipmentRole'] else {})
+
+ if policy_property.get('attributes'):
+ for attr_key, attr_val in policy_property['attributes'].items():
+ update_converted_attribute(attr_key, attr_val, prop)
+
prop.update(get_candidates_demands(demand))
demand_properties.append(prop)
return demand_properties
+def update_converted_attribute(attr_key, attr_val, properties):
+ """
+ Updates dictonary of attributes with one specified in the arguments.
+ Automatically translates key namr from camelCase to hyphens
+ :param attr_key: key of the attribute
+ :param attr_val: value of the attribute
+ :param properties: dictionary with attributes to update
+ :return:
+ """
+ if attr_val:
+ remapping = policy_config_mapping['attributes']
+ if remapping.get(attr_key):
+ key_value = remapping.get(attr_key)
+ else:
+ key_value = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', attr_key)
+ key_value = re.sub('([a-z0-9])([A-Z])', r'\1-\2', key_value).lower()
+ properties['attributes'].update({key_value: attr_val})
+
+
def gen_demands(req_json, vnf_policies):
"""Generate list of demands based on request and VNF policies
:param req_json: Request object from the client (e.g. MSO)
diff --git a/osdf/templates/conductor_interface.json b/osdf/templates/conductor_interface.json
index 7377c48..0b8e6a1 100755
--- a/osdf/templates/conductor_interface.json
+++ b/osdf/templates/conductor_interface.json
@@ -1,41 +1,39 @@
-{
- "name": "{{ name }}",
- "files": {},
- "timeout": {{ timeout }},
- "limit": {{ limit }},
- "template": {
- "homing_template_version": "2017-10-10",
- "parameters": {
- "service_name": "{{ service_type }}",
- "service_id": "{{ service_id }}",
- "customer_lat": {{ latitude }},
- "customer_long": {{ longitude }},
- "REQUIRED_DISK": "{{ required_disk }}",
- "REQUIRED_MEM": "{{ required_mem }}"
- },
- "locations": {
- "customer_loc": {
- "latitude": { "get_param": "customer_lat" },
- "longitude": { "get_param": "customer_long" }
- }
- },
- "demands": {{ json.dumps(demand_list) }},
- {% set comma_main = joiner(",") %}
- "constraints": {
- {% set comma=joiner(",") %}
- {% for elem in policy_groups %} {{ comma() }}
- {% for key, value in elem.items() %}
- "{{key}}": {{ json.dumps(value) }}
- {% endfor %}
- {% endfor %}
- },
- "optimization": {
- {% set comma=joiner(",") %}
- {% for elem in optimization_policies %} {{ comma() }}
- {% for key, value in elem.items() %}
- "{{key}}": {{ json.dumps(value) }}
- {% endfor %}
- {% endfor %}
- }
- }
-}
+{
+ "name": "{{ name }}",
+ "files": {},
+ "timeout": {{ timeout }},
+ "limit": {{ limit }},
+ "template": {
+ "homing_template_version": "2017-10-10",
+ "parameters": {
+ {% set comma=joiner(",") %}
+ {% for key, value in request_params.items() %} {{ comma() }}
+ "{{key}}": {{ json.dumps(value) }}
+ {% endfor %}
+ },
+ "locations": {
+ "customer_loc": {
+ "latitude": { "get_param": "customer_lat" },
+ "longitude": { "get_param": "customer_long" }
+ }
+ },
+ "demands": {{ json.dumps(demand_list) }},
+ {% set comma_main = joiner(",") %}
+ "constraints": {
+ {% set comma=joiner(",") %}
+ {% for elem in policy_groups %} {{ comma() }}
+ {% for key, value in elem.items() %}
+ "{{key}}": {{ json.dumps(value) }}
+ {% endfor %}
+ {% endfor %}
+ },
+ "optimization": {
+ {% set comma=joiner(",") %}
+ {% for elem in optimization_policies %} {{ comma() }}
+ {% for key, value in elem.items() %}
+ "{{key}}": {{ json.dumps(value) }}
+ {% endfor %}
+ {% endfor %}
+ }
+ }
+}
diff --git a/test/conductor/test_conductor_calls.py b/test/conductor/test_conductor_calls.py
index 1a96da7..77d9a72 100644
--- a/test/conductor/test_conductor_calls.py
+++ b/test/conductor/test_conductor_calls.py
@@ -43,6 +43,11 @@ class TestConductorCalls(unittest.TestCase):
policies = pol.get_local_policies("test/policy-local-files/", self.lp)
conductor.request(req_json, self.osdf_config, policies)
+ def test_request_vfmod(self):
+ req_json = json_from_file("./test/placement-tests/request_vfmod.json")
+ policies = pol.get_local_policies("test/policy-local-files/", self.lp)
+ conductor.request(req_json, self.osdf_config, policies)
+
if __name__ == "__main__":
unittest.main()
diff --git a/test/conductor/test_conductor_translation.py b/test/conductor/test_conductor_translation.py
index 0c7da94..27711f5 100644
--- a/test/conductor/test_conductor_translation.py
+++ b/test/conductor/test_conductor_translation.py
@@ -28,16 +28,18 @@ from osdf.utils.interfaces import json_from_file, yaml_from_file
class TestConductorTranslation(unittest.TestCase):
def setUp(self):
- main_dir = ""
- conductor_api_template = main_dir + "osdf/templates/conductor_interface.json"
- parameter_data_file = main_dir + "test/placement-tests/request.json"
- policy_data_path = main_dir + "test/policy-local-files/"
- local_config_file = main_dir + "config/common_config.yaml"
+ self.main_dir = ""
+ self.conductor_api_template = self.main_dir + "osdf/templates/conductor_interface.json"
+ self.local_config_file = self.main_dir + "config/common_config.yaml"
+ policy_data_path = self.main_dir + "test/policy-local-files"
valid_policies_list_file = policy_data_path + '/' + 'meta-valid-policies.txt'
valid_policies_files = local_policies.get_policy_names_from_file(valid_policies_list_file)
+ parameter_data_file = self.main_dir + "test/placement-tests/request.json"
self.request_json = json_from_file(parameter_data_file)
+ parameter_data_file = self.main_dir + "test/placement-tests/request_vfmod.json"
+ self.request_vfmod_json = json_from_file(parameter_data_file)
self.policies = [json_from_file(policy_data_path + '/' + name) for name in valid_policies_files]
def tearDown(self):
@@ -49,6 +51,12 @@ class TestConductorTranslation(unittest.TestCase):
res = tr.gen_demands(self.request_json, vnf_policies)
assert res is not None
+ def test_gen_vfmod_demands(self):
+ # need to run this only on vnf policies
+ vnf_policies = [x for x in self.policies if x["content"]["policyType"] == "vnfPolicy"]
+ res = tr.gen_demands(self.request_vfmod_json, vnf_policies)
+ assert res is not None
+
if __name__ == "__main__":
unittest.main()
diff --git a/test/placement-tests/request_placement_vfmod.json b/test/placement-tests/request_placement_vfmod.json
new file mode 100644
index 0000000..4233416
--- /dev/null
+++ b/test/placement-tests/request_placement_vfmod.json
@@ -0,0 +1,88 @@
+{
+ "name": "de4f04e3-0a65-470b-9d07-8ea6c2fb3e10",
+ "template": {
+ "constraints": {
+ "affinity_vFW_TD": {
+ "demands": ["vFW-SINK", "vPGN"],
+ "properties": {
+ "category": "region",
+ "qualifier": "same"
+ },
+ "type": "zone"
+ }
+ },
+ "parameters": {
+ "service_name": "vFW_TD",
+ "chosen_region": "RegionOne",
+ "service_id": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c",
+ "customer_long": 2.2,
+ "REQUIRED_MEM": "",
+ "customer_lat": 1.1,
+ "REQUIRED_DISK": ""
+ },
+ "locations": {
+ "customer_loc": {
+ "longitude": {
+ "get_param": "customer_long"
+ },
+ "latitude": {
+ "get_param": "customer_lat"
+ }
+ }
+ },
+ "demands": {
+ "vFW-SINK": [{
+ "attributes": {
+ "global-customer-id": "Demonstration",
+ "cloud-region-id": {
+ "get_param": "chosen_region"
+ },
+ "model-version-id": "763731df-84fd-494b-b824-01fc59a5ff2d",
+ "orchestration-status": ["active"],
+ "model-invariant-id": "e7227847-dea6-4374-abca-4561b070fe7d",
+ "service_instance_id": {
+ "get_param": "service_id"
+ },
+ "prov-status": "ACTIVE"
+ },
+ "inventory_provider": "aai",
+ "service_resource_id": "vFW-SINK-XX",
+ "inventory_type": "vfmodule",
+ "service_type": "vFW-SINK-XX",
+ "excluded_candidates": [{
+ "inventory_type": "vfmodule",
+ "candidate_id": ["e765d576-8755-4145-8536-0bb6d9b1dc9a"]
+ }]
+ }],
+ "vPGN": [{
+ "attributes": {
+ "global-customer-id": "Demonstration",
+ "cloud-region-id": {
+ "get_param": "chosen_region"
+ },
+ "model-version-id": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b",
+ "orchestration-status": ["active"],
+ "model-invariant-id": "762472ef-5284-4daa-ab32-3e7bee2ec355",
+ "service_instance_id": {
+ "get_param": "service_id"
+ },
+ "prov-status": "ACTIVE"
+ },
+ "inventory_provider": "aai",
+ "service_resource_id": "vPGN-XX",
+ "unique": "false",
+ "inventory_type": "vfmodule",
+ "service_type": "vPGN-XX"
+ }]
+ },
+ "optimization": {
+ "minimize": {
+ "sum": []
+ }
+ },
+ "homing_template_version": "2017-10-10"
+ },
+ "limit": 100,
+ "files": {},
+ "timeout": 1200
+} \ No newline at end of file
diff --git a/test/placement-tests/request_vfmod.json b/test/placement-tests/request_vfmod.json
new file mode 100644
index 0000000..1e95e22
--- /dev/null
+++ b/test/placement-tests/request_vfmod.json
@@ -0,0 +1,58 @@
+{
+ "requestInfo": {
+ "transactionId": "e576c75e-7536-4145-a1c0-d60b65bb1bb8",
+ "requestId": "de4f04e3-0a65-470b-9d07-8ea6c2fb3e10",
+ "callbackUrl": "http://0.0.0.0:9000/osdfCallback/",
+ "sourceId": "SO",
+ "requestType": "create",
+ "numSolutions": "100",
+ "optimizers": [
+ "placement"
+ ],
+ "timeout": 1200
+ },
+ "placementInfo": {
+ "requestParameters": {
+ "chosenRegion": "RegionOne"
+ },
+ "subscriberInfo": {
+ "globalSubscriberId": "dbc2c763-6383-42d6-880a-b7d5c5bc84d9",
+ "subscriberName": "oof-so-chm"
+ },
+ "placementDemands": [
+ {
+ "resourceModuleName": "vFW-SINK",
+ "serviceResourceId": "vFW-SINK-XX",
+ "resourceModelInfo": {
+ "modelInvariantId": "e7227847-dea6-4374-abca-4561b070fe7d",
+ "modelVersionId": "763731df-84fd-494b-b824-01fc59a5ff2d"
+ },
+ "excludedCandidates": [
+ {
+ "identifierType": "vfmodule",
+ "identifiers": [
+ "e765d576-8755-4145-8536-0bb6d9b1dc9a"
+ ]
+ }
+ ]
+ },
+ {
+ "resourceModuleName": "vPGN",
+ "serviceResourceId": "vPGN-XX",
+ "unique": "false",
+ "resourceModelInfo": {
+ "modelInvariantId": "762472ef-5284-4daa-ab32-3e7bee2ec355",
+ "modelVersionId": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b"
+ }
+ }
+ ]
+ },
+ "serviceInfo": {
+ "serviceInstanceId": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c",
+ "serviceName": "vFW_TD",
+ "modelInfo": {
+ "modelInvariantId": "TD-invariantId",
+ "modelVersionId": "TD-versionId"
+ }
+ }
+} \ No newline at end of file
diff --git a/test/placement-tests/response_vfmod.json b/test/placement-tests/response_vfmod.json
new file mode 100644
index 0000000..06e40de
--- /dev/null
+++ b/test/placement-tests/response_vfmod.json
@@ -0,0 +1,229 @@
+{
+ "transactionId": "",
+ "requestStatus": "completed",
+ "solutions": {
+ "placementSolutions": [
+ [{
+ "assignmentInfo": [{
+ "key": "locationType",
+ "value": "att_aic"
+ }, {
+ "key": "vnfHostName",
+ "value": "vFW-FW-MC"
+ }, {
+ "key": "locationId",
+ "value": "RegionOne"
+ }, {
+ "key": "isRehome",
+ "value": "false"
+ }, {
+ "key": "nf-name",
+ "value": "vFW-FW-MC"
+ }, {
+ "key": "vnf-type",
+ "value": "5G_EVE_Demo/5G_EVE_FW 0"
+ }, {
+ "key": "ipv6-oam-address",
+ "value": ""
+ }, {
+ "key": "ipv4-oam-address",
+ "value": "oam_network_zb4J"
+ }, {
+ "key": "vservers",
+ "value": [{
+ "vserver-id": "4a61b075-5ae0-4cfe-b213-27d3647a0578",
+ "l-interfaces": [{
+ "macaddr": "fa:16:3e:4a:00:56",
+ "interface-name": "vnf-snk-r1-t1-mc-vfw_private_3_port-tntiamoj2res",
+ "ipv4-addresses": ["10.100.100.1"],
+ "ipv6-addresses": [],
+ "interface-id": "ff27775b-a2b7-4e6e-8f71-6a5a5e6020cd",
+ "network-name": "",
+ "network-id": "59763a33-3296-4dc8-9ee6-2bdcd63322fc"
+ }, {
+ "macaddr": "fa:16:3e:a1:e8:c9",
+ "interface-name": "vnf-snk-r1-t1-mc-vfw_private_2_port-hiay5zan4da6",
+ "ipv4-addresses": ["10.0.110.1"],
+ "ipv6-addresses": [],
+ "interface-id": "0bb0bb92-a4d1-4104-b491-e469949f60a3",
+ "network-name": "",
+ "network-id": "cdb4bc25-2412-4b77-bbd5-791a02f8776d"
+ }, {
+ "macaddr": "fa:16:3e:45:e2:16",
+ "interface-name": "vnf-snk-r1-t1-mc-vfw_private_0_port-7xlr5kjvsmk6",
+ "ipv4-addresses": ["192.168.10.100"],
+ "ipv6-addresses": [],
+ "interface-id": "f0291365-6070-4baa-8470-8775bed7c2c4",
+ "network-name": "",
+ "network-id": "932ac514-639a-45b2-b1a3-4c5bb708b5c1"
+ }, {
+ "macaddr": "fa:16:3e:2f:0b:2f",
+ "interface-name": "vnf-snk-r1-t1-mc-vfw_private_1_port-khio4swt2vy3",
+ "ipv4-addresses": ["192.168.20.100"],
+ "ipv6-addresses": [],
+ "interface-id": "5ba290b0-0833-4008-acda-be1878b9ae0c",
+ "network-name": "",
+ "network-id": "bd64a2b0-0bdd-45b4-b755-65d5ebe1cee0"
+ }],
+ "vserver-name": "vfw-vfw-1-dt"
+ }, {
+ "vserver-id": "cf51eeab-8f75-4635-a01c-9f4bbd1e146e",
+ "l-interfaces": [{
+ "macaddr": "fa:16:3e:23:82:d7",
+ "interface-name": "vnf-snk-r1-t1-mc-vsn_private_2_port-spbtqjnybz5g",
+ "ipv4-addresses": ["10.100.100.3"],
+ "ipv6-addresses": [],
+ "interface-id": "1b3fd313-cde3-4df6-8ea8-bf4ae28e7e03",
+ "network-name": "",
+ "network-id": "59763a33-3296-4dc8-9ee6-2bdcd63322fc"
+ }, {
+ "macaddr": "fa:16:3e:fc:bd:16",
+ "interface-name": "vnf-snk-r1-t1-mc-vsn_private_1_port-spqyrticfqan",
+ "ipv4-addresses": ["10.0.110.3"],
+ "ipv6-addresses": [],
+ "interface-id": "1b33d675-f351-4766-8669-7314f774d52c",
+ "network-name": "",
+ "network-id": "cdb4bc25-2412-4b77-bbd5-791a02f8776d"
+ }, {
+ "macaddr": "fa:16:3e:3d:e9:c5",
+ "interface-name": "vnf-snk-r1-t1-mc-vsn_private_0_port-5ijwpdueh2fl",
+ "ipv4-addresses": ["192.168.20.250"],
+ "ipv6-addresses": [],
+ "interface-id": "cf82e256-8ccf-4e43-ba96-04ea2e47b5d2",
+ "network-name": "",
+ "network-id": "bd64a2b0-0bdd-45b4-b755-65d5ebe1cee0"
+ }],
+ "vserver-name": "vfw-vsn-1-dt"
+ }]
+ }, {
+ "key": "nf-type",
+ "value": "vnf"
+ }, {
+ "key": "vnfHostName",
+ "value": "vFW-FW-MC"
+ }, {
+ "key": "aic_version",
+ "value": "1"
+ }, {
+ "key": "cloudClli",
+ "value": "clli1"
+ }, {
+ "key": "service_instance_id",
+ "value": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c"
+ }, {
+ "key": "cloudOwner",
+ "value": "CloudOwner"
+ }, {
+ "value": "vfw-0-mc",
+ "key": "vf-module-name"
+ }, {
+ "value": "85eec994-b635-42c7-87a1-d39720cad36d",
+ "key": "vf-module-id"
+ }, {
+ "key": "nf-id",
+ "value": "4d2dc294-dbb3-44a2-8422-fa61b30c21a9"
+ }],
+ "serviceResourceId": "vFW-SINK-XX",
+ "solution": {
+ "cloudOwner": "CloudOwner",
+ "identifiers": ["85eec994-b635-42c7-87a1-d39720cad36d"],
+ "identifierType": "vfmodule"
+ },
+ "resourceModuleName": "vFW-SINK"
+ }, {
+ "assignmentInfo": [{
+ "key": "locationType",
+ "value": "att_aic"
+ }, {
+ "key": "vnfHostName",
+ "value": "vFW-PKG-MC"
+ }, {
+ "key": "locationId",
+ "value": "RegionOne"
+ }, {
+ "key": "isRehome",
+ "value": "false"
+ }, {
+ "key": "nf-name",
+ "value": "vFW-PKG-MC"
+ }, {
+ "key": "vnf-type",
+ "value": "5G_EVE_Demo/5G_EVE_PKG 0"
+ }, {
+ "key": "ipv6-oam-address",
+ "value": ""
+ }, {
+ "key": "ipv4-oam-address",
+ "value": "oam_network_zb4J"
+ }, {
+ "key": "vservers",
+ "value": [{
+ "vserver-id": "00bddefc-126e-4e4f-a18d-99b94d8d9a30",
+ "l-interfaces": [{
+ "macaddr": "fa:16:3e:c4:07:7f",
+ "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_2_port-mf7lu55usq7i",
+ "ipv4-addresses": ["10.100.100.2"],
+ "ipv6-addresses": [],
+ "interface-id": "4b333af1-90d6-42ae-8389-d440e6ff0e93",
+ "network-name": "",
+ "network-id": "59763a33-3296-4dc8-9ee6-2bdcd63322fc"
+ }, {
+ "macaddr": "fa:16:3e:b5:86:38",
+ "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_1_port-734xxixicw6r",
+ "ipv4-addresses": ["10.0.110.2"],
+ "ipv6-addresses": [],
+ "interface-id": "85dd57e9-6e3a-48d0-a784-4598d627e798",
+ "network-name": "",
+ "network-id": "cdb4bc25-2412-4b77-bbd5-791a02f8776d"
+ }, {
+ "macaddr": "fa:16:3e:ff:d8:6f",
+ "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_0_port-e5qdm3p5ijhe",
+ "ipv4-addresses": ["192.168.10.200"],
+ "ipv6-addresses": [],
+ "interface-id": "edaff25a-878e-4706-ad52-4e3d51cf6a82",
+ "network-name": "",
+ "network-id": "932ac514-639a-45b2-b1a3-4c5bb708b5c1"
+ }],
+ "vserver-name": "zdfw1fwl01pgn01"
+ }]
+ }, {
+ "key": "nf-type",
+ "value": "vnf"
+ }, {
+ "key": "vnfHostName",
+ "value": "vFW-PKG-MC"
+ }, {
+ "key": "aic_version",
+ "value": "1"
+ }, {
+ "key": "cloudClli",
+ "value": "clli1"
+ }, {
+ "key": "service_instance_id",
+ "value": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c"
+ }, {
+ "key": "cloudOwner",
+ "value": "CloudOwner"
+ }, {
+ "value": "pkg-0-mc",
+ "key": "vf-module-name"
+ }, {
+ "value": "d187d743-5932-4fb9-a42d-db0a5be5ba7e",
+ "key": "vf-module-id"
+ }, {
+ "key": "nf-id",
+ "value": "fcbff633-47cc-4f38-a98d-4ba8285bd8b6"
+ }],
+ "serviceResourceId": "vPGN-XX",
+ "solution": {
+ "cloudOwner": "CloudOwner",
+ "identifiers": ["d187d743-5932-4fb9-a42d-db0a5be5ba7e"],
+ "identifierType": "vfmodule"
+ },
+ "resourceModuleName": "vPGN"
+ }]
+ ]
+ },
+ "statusMessage": "",
+ "requestId": "de4f04e3-0a65-470b-9d07-8ea6c2fb3e10"
+} \ No newline at end of file
diff --git a/test/policy-local-files/QueryPolicy_vFW_TD.json b/test/policy-local-files/QueryPolicy_vFW_TD.json
new file mode 100644
index 0000000..dcf7439
--- /dev/null
+++ b/test/policy-local-files/QueryPolicy_vFW_TD.json
@@ -0,0 +1,30 @@
+{
+ "service": "queryPolicy",
+ "policyName": "OSDF_DUBLIN.QueryPolicy_vFW_TD",
+ "description": "Query policy for vFW TD",
+ "templateVersion": "OpenSource.version.1",
+ "version": "oofDublin",
+ "priority": "3",
+ "riskType": "test",
+ "riskLevel": "2",
+ "guard": "False",
+ "content": {
+ "queryProperties": [
+ {"attribute":"customerLatitude", "attribute_location": "customerLatitude", "value": 1.1},
+ {"attribute":"customerLongitude", "attribute_location": "customerLongitude", "value": 2.2},
+ {"attribute":"chosen_region", "attribute_location": "chosenRegion"}
+ ],
+ "policyScope": [
+ "TD",
+ "vFW-SINK",
+ "vPGN"
+ ],
+ "policyType": "request_param_query",
+ "serviceName": "vFW_TD",
+ "identity": "vFW_TD_Query_Policy",
+ "resources": [
+ "vFW-SINK",
+ "vPGN"
+ ]
+ }
+}
diff --git a/test/policy-local-files/affinity_vFW_TD.json b/test/policy-local-files/affinity_vFW_TD.json
new file mode 100644
index 0000000..371cbfc
--- /dev/null
+++ b/test/policy-local-files/affinity_vFW_TD.json
@@ -0,0 +1,28 @@
+{
+ "service": "affinityPolicy",
+ "policyName": "OSDF_DUBLIN.Affinity_vFW_TD",
+ "description": "Affinity policy for vPGN Anchor and vFW destination point",
+ "templateVersion": "OpenSource.version.1",
+ "version": "oofDublin",
+ "priority": "3",
+ "riskType": "test",
+ "riskLevel": "2",
+ "guard": "False",
+ "content": {
+ "identity": "affinity_vFW_TD",
+ "policyScope": [
+ "TD",
+ "vFW-SINK",
+ "vPGN"
+ ],
+ "affinityProperty": {
+ "qualifier": "same",
+ "category": "region"
+ },
+ "policyType": "zone",
+ "resources": [
+ "vFW-SINK",
+ "vPGN"
+ ]
+ }
+} \ No newline at end of file
diff --git a/test/policy-local-files/meta-valid-policies.txt b/test/policy-local-files/meta-valid-policies.txt
index 772ec1a..99e3e88 100644
--- a/test/policy-local-files/meta-valid-policies.txt
+++ b/test/policy-local-files/meta-valid-policies.txt
@@ -10,3 +10,7 @@ hpa_policy_vGMuxInfra_1.json
hpa_policy_vG_1.json
vnfPolicy_vG.json
vnfPolicy_vGMuxInfra.json
+QueryPolicy_vFW_TD.json
+vnfPolicy_vFW_TD.json
+vnfPolicy_vPGN_TD.json
+affinity_vFW_TD.json \ No newline at end of file
diff --git a/test/policy-local-files/vnfPolicy_vFW_TD.json b/test/policy-local-files/vnfPolicy_vFW_TD.json
new file mode 100644
index 0000000..efe8ffa
--- /dev/null
+++ b/test/policy-local-files/vnfPolicy_vFW_TD.json
@@ -0,0 +1,35 @@
+{
+ "service": "vnfPolicy",
+ "policyName": "OSDF_DUBLIN.vnfPolicy_vFW_TD",
+ "description": "vnfPolicy",
+ "templateVersion": "OpenSource.version.1",
+ "version": "oofDublin",
+ "priority": "6",
+ "riskType": "test",
+ "riskLevel": "3",
+ "guard": "False",
+ "content": {
+ "identity": "vnf_vFW_TD",
+ "policyScope": ["TD", "vFW-SINK"],
+ "policyType": "vnfPolicy",
+ "resources": ["vFW-SINK"],
+ "applicableResources": "any",
+ "vnfProperties": [{
+ "inventoryProvider": "aai",
+ "serviceType": "",
+ "inventoryType": "vfmodule",
+ "customerId": "Demonstration",
+ "equipmentRole": "",
+ "attributes": {
+ "orchestrationStatus": ["active"],
+ "provStatus": "ACTIVE",
+ "cloudRegionId": {
+ "get_param": "chosen_region"
+ },
+ "service_instance_id": {
+ "get_param": "service_id"
+ }
+ }
+ }]
+ }
+} \ No newline at end of file
diff --git a/test/policy-local-files/vnfPolicy_vPGN_TD.json b/test/policy-local-files/vnfPolicy_vPGN_TD.json
new file mode 100644
index 0000000..64740ce
--- /dev/null
+++ b/test/policy-local-files/vnfPolicy_vPGN_TD.json
@@ -0,0 +1,35 @@
+{
+ "service": "vnfPolicy",
+ "policyName": "OSDF_DUBLIN.vnfPolicy_vPGN_TD",
+ "description": "vnfPolicy",
+ "templateVersion": "OpenSource.version.1",
+ "version": "oofDublin",
+ "priority": "6",
+ "riskType": "test",
+ "riskLevel": "3",
+ "guard": "False",
+ "content": {
+ "identity": "vnf_vPGN_TD",
+ "policyScope": ["TD", "vPGN"],
+ "policyType": "vnfPolicy",
+ "resources": ["vPGN"],
+ "applicableResources": "any",
+ "vnfProperties": [{
+ "inventoryProvider": "aai",
+ "serviceType": "",
+ "inventoryType": "vfmodule",
+ "customerId": "Demonstration",
+ "equipmentRole": "",
+ "attributes": {
+ "orchestrationStatus": ["active"],
+ "provStatus": "ACTIVE",
+ "cloudRegionId": {
+ "get_param": "chosen_region"
+ },
+ "service_instance_id": {
+ "get_param": "service_id"
+ }
+ }
+ }]
+ }
+} \ No newline at end of file
diff --git a/test/test_ConductorApiBuilder.py b/test/test_ConductorApiBuilder.py
index f8683a3..7e38f4d 100644
--- a/test/test_ConductorApiBuilder.py
+++ b/test/test_ConductorApiBuilder.py
@@ -28,28 +28,38 @@ class TestConductorApiBuilder(unittest.TestCase):
def setUp(self):
self.main_dir = ""
- conductor_api_template = self.main_dir + "osdf/templates/conductor_interface.json"
- parameter_data_file = self.main_dir + "test/placement-tests/request.json" # "test/placement-tests/request.json"
+ self.conductor_api_template = self.main_dir + "osdf/templates/conductor_interface.json"
+ self.local_config_file = self.main_dir + "config/common_config.yaml"
policy_data_path = self.main_dir + "test/policy-local-files" # "test/policy-local-files"
- local_config_file = self.main_dir + "config/common_config.yaml"
valid_policies_list_file = policy_data_path + '/' + 'meta-valid-policies.txt'
valid_policies_files = local_policies.get_policy_names_from_file(valid_policies_list_file)
+ parameter_data_file = self.main_dir + "test/placement-tests/request.json" # "test/placement-tests/request.json"
self.request_json = json_from_file(parameter_data_file)
+ parameter_data_file = self.main_dir + "test/placement-tests/request_vfmod.json"
+ self.request_vfmod_json = json_from_file(parameter_data_file)
+ parameter_data_file = self.main_dir + "test/placement-tests/request_placement_vfmod.json"
+ self.request_placement_vfmod_json = json_from_file(parameter_data_file)
self.policies = [json_from_file(policy_data_path + '/' + name) for name in valid_policies_files]
def test_conductor_api_call_builder(self):
main_dir = self.main_dir
- conductor_api_template = main_dir + "osdf/templates/conductor_interface.json" # "osdf/templates/conductor_interface.json"
- local_config_file = main_dir + "config/common_config.yaml"
request_json = self.request_json
policies = self.policies
- local_config = yaml.load(open(local_config_file))
- templ_string = conductor_api_builder(request_json, policies, local_config, conductor_api_template)
+ local_config = yaml.load(open(self.local_config_file))
+ templ_string = conductor_api_builder(request_json, policies, local_config, self.conductor_api_template)
templ_json = json.loads(templ_string)
self.assertEqual(templ_json["name"], "yyy-yyy-yyyy")
+ def test_conductor_api_call_builder_vfmod(self):
+ request_json = self.request_vfmod_json
+ policies = self.policies
+ local_config = yaml.load(open(self.local_config_file))
+ templ_string = conductor_api_builder(request_json, policies, local_config, self.conductor_api_template)
+ templ_json = json.loads(templ_string)
+ self.assertEqual(templ_json, self.request_placement_vfmod_json)
+
if __name__ == "__main__":
unittest.main()
diff --git a/test/test_api_validation.py b/test/test_api_validation.py
index 80e0ba0..389ff62 100644
--- a/test/test_api_validation.py
+++ b/test/test_api_validation.py
@@ -30,6 +30,11 @@ class TestReqValidation(unittest.TestCase):
req_json = json.loads(open(req_file).read())
self.assertEqual(PlacementAPI(req_json).validate(), None)
+ def test_req_vfmod_validation(self):
+ req_file = "./test/placement-tests/request_vfmod.json"
+ req_json = json.loads(open(req_file).read())
+ self.assertEqual(PlacementAPI(req_json).validate(), None)
+
def test_req_failure(self):
req_json = {}
self.assertRaises(ModelValidationError, lambda: PlacementAPI(req_json).validate())
@@ -42,6 +47,11 @@ class TestResponseValidation(unittest.TestCase):
req_json = json.loads(open(req_file).read())
self.assertEqual(PlacementResponse(req_json).validate(), None)
+ def test_res_vfmod_validation(self):
+ req_file = "./test/placement-tests/response_vfmod.json"
+ req_json = json.loads(open(req_file).read())
+ self.assertEqual(PlacementResponse(req_json).validate(), None)
+
def test_invalid_response(self):
resp_json = {}
self.assertRaises(ModelValidationError, lambda: PlacementResponse(resp_json).validate())
diff --git a/test/test_get_opt_query_data.py b/test/test_get_opt_query_data.py
index 880f93f..1e2db17 100644
--- a/test/test_get_opt_query_data.py
+++ b/test/test_get_opt_query_data.py
@@ -1,40 +1,52 @@
-# -------------------------------------------------------------------------
-# Copyright (c) 2017-2018 AT&T Intellectual Property
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# -------------------------------------------------------------------------
-#
-import unittest
-import json
-from osdf.optimizers.placementopt.conductor.translation import get_opt_query_data
-
-
-class TestGetOptQueryData(unittest.TestCase):
-
- def test_get_opt_query_data(self):
- main_dir = ""
- parameter_data_file = main_dir + "test/placement-tests/request.json"
- policy_data_path = main_dir + "test/policy-local-files/"
-
- query_policy_data_file = ["QueryPolicy_vCPE.json"]
- request_json = json.loads(open(parameter_data_file).read())
- policies = [json.loads(open(policy_data_path + file).read()) for file in query_policy_data_file]
- req_param_dict = get_opt_query_data(request_json, policies)
-
- self.assertTrue(req_param_dict is not None)
-
-
-if __name__ == "__main__":
- unittest.main()
-
+# -------------------------------------------------------------------------
+# Copyright (c) 2017-2018 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+import unittest
+import json
+from osdf.optimizers.placementopt.conductor.translation import get_opt_query_data
+
+
+class TestGetOptQueryData(unittest.TestCase):
+
+ def test_get_opt_query_data(self):
+ main_dir = ""
+ parameter_data_file = main_dir + "test/placement-tests/request.json"
+ policy_data_path = main_dir + "test/policy-local-files/"
+
+ query_policy_data_file = ["QueryPolicy_vCPE.json"]
+ request_json = json.loads(open(parameter_data_file).read())
+ policies = [json.loads(open(policy_data_path + file).read()) for file in query_policy_data_file]
+ req_param_dict = get_opt_query_data(request_json, policies)
+
+ self.assertTrue(req_param_dict is not None)
+
+ def test_get_opt_query_data_vfmod(self):
+ main_dir = ""
+ parameter_data_file = main_dir + "test/placement-tests/request_vfmod.json"
+ policy_data_path = main_dir + "test/policy-local-files/"
+
+ query_policy_data_file = ["QueryPolicy_vFW_TD.json"]
+ request_json = json.loads(open(parameter_data_file).read())
+ policies = [json.loads(open(policy_data_path + file).read()) for file in query_policy_data_file]
+ req_param_dict = get_opt_query_data(request_json, policies)
+
+ self.assertTrue(req_param_dict is not None)
+
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/test/test_process_placement_opt.py b/test/test_process_placement_opt.py
index 3219675..62a1ce6 100644
--- a/test/test_process_placement_opt.py
+++ b/test/test_process_placement_opt.py
@@ -76,6 +76,20 @@ class TestProcessPlacementOpt(unittest.TestCase):
local_config = yaml_from_file(local_config_file)
templ_string = process_placement_opt(request_json, policies, local_config)
+ def test_process_placement_opt_placementDemand_vfmodule(self):
+ main_dir = ""
+ parameter_data_file = main_dir + "test/placement-tests/request_vfmod.json"
+ policy_data_path = main_dir + "test/policy-local-files/"
+ local_config_file = main_dir + "config/common_config.yaml"
+
+ valid_policies_list_file = policy_data_path + '/' + 'meta-valid-policies.txt'
+ valid_policies_files = local_policies.get_policy_names_from_file(valid_policies_list_file)
+
+ request_json = json_from_file(parameter_data_file)
+ policies = [json_from_file(policy_data_path + '/' + name) for name in valid_policies_files]
+ local_config = yaml_from_file(local_config_file)
+ templ_string = process_placement_opt(request_json, policies, local_config)
+
if __name__ == "__main__":
unittest.main()