Merge branch 'WIM' into master
Change-Id: Ibef33c7dfcf3c40dbdf278fc160907675ffbeb03
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
diff --git a/.gitignore-common b/.gitignore-common
index 92edf1b..85235b2 100644
--- a/.gitignore-common
+++ b/.gitignore-common
@@ -22,6 +22,9 @@
# This is a template with common files to be igonored, after clone make a copy to .gitignore
# cp .gitignore-common .gitignore
+.tox/
+.coverage
+
*.pyc
*.pyo
diff --git a/database_utils/migrate_mano_db.sh b/database_utils/migrate_mano_db.sh
index ecd6b11..aa2e718 100755
--- a/database_utils/migrate_mano_db.sh
+++ b/database_utils/migrate_mano_db.sh
@@ -24,6 +24,7 @@
#
#Upgrade/Downgrade openmano database preserving the content
#
+DBUTILS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DBUSER="mano"
DBPASS=""
@@ -33,7 +34,7 @@
DBNAME="mano_db"
QUIET_MODE=""
#TODO update it with the last database version
-LAST_DB_VERSION=33
+LAST_DB_VERSION=34
# Detect paths
MYSQL=$(which mysql)
@@ -201,6 +202,7 @@
#[ $OPENMANO_VER_NUM -ge 5061 ] && DB_VERSION=31 #0.5.61 => 31
#[ $OPENMANO_VER_NUM -ge 5070 ] && DB_VERSION=32 #0.5.70 => 32
#[ $OPENMANO_VER_NUM -ge 5082 ] && DB_VERSION=33 #0.5.82 => 33
+#[ $OPENMANO_VER_NUM -ge 6000 ] && DB_VERSION=34 #0.6.00 => 34
#TODO ... put next versions here
function upgrade_to_1(){
@@ -222,8 +224,8 @@
}
function downgrade_from_1(){
# echo " downgrade database from version 0.1 to version 0.0"
- echo " DROP TABLE \`schema_version\`"
- sql "DROP TABLE \`schema_version\`;"
+ echo " DROP TABLE IF EXISTS \`schema_version\`"
+ sql "DROP TABLE IF EXISTS \`schema_version\`;"
}
function upgrade_to_2(){
# echo " upgrade database from version 0.1 to version 0.2"
@@ -304,11 +306,11 @@
echo " Delete columns 'user/passwd' from 'vim_tenants'"
sql "ALTER TABLE vim_tenants DROP COLUMN user, DROP COLUMN passwd; "
echo " delete tables 'datacenter_images', 'images'"
- sql "DROP TABLE \`datacenters_images\`;"
- sql "DROP TABLE \`images\`;"
+ sql "DROP TABLE IF EXISTS \`datacenters_images\`;"
+ sql "DROP TABLE IF EXISTS \`images\`;"
echo " delete tables 'datacenter_flavors', 'flavors'"
- sql "DROP TABLE \`datacenters_flavors\`;"
- sql "DROP TABLE \`flavors\`;"
+ sql "DROP TABLE IF EXISTS \`datacenters_flavors\`;"
+ sql "DROP TABLE IF EXISTS \`flavors\`;"
sql "DELETE FROM schema_version WHERE version_int='2';"
}
@@ -622,7 +624,7 @@
function downgrade_from_12(){
# echo " downgrade database from version 0.12 to version 0.11"
echo " delete ip_profiles table, and remove ip_address column in 'interfaces' and 'sce_interfaces'"
- sql "DROP TABLE ip_profiles;"
+ sql "DROP TABLE IF EXISTS ip_profiles;"
sql "ALTER TABLE interfaces DROP COLUMN ip_address;"
sql "ALTER TABLE sce_interfaces DROP COLUMN ip_address;"
sql "DELETE FROM schema_version WHERE version_int='12';"
@@ -1006,8 +1008,8 @@
"REFERENCES scenarios (uuid);"
echo " Delete table instance_actions"
- sql "DROP TABLE vim_actions"
- sql "DROP TABLE instance_actions"
+ sql "DROP TABLE IF EXISTS vim_actions"
+ sql "DROP TABLE IF EXISTS instance_actions"
sql "DELETE FROM schema_version WHERE version_int='26';"
}
@@ -1220,24 +1222,24 @@
function downgrade_from_28(){
echo " [Undo adding the VNFFG tables]"
echo " Dropping instance_sfps"
- sql "DROP TABLE instance_sfps;"
+ sql "DROP TABLE IF EXISTS instance_sfps;"
echo " Dropping sce_classifications"
- sql "DROP TABLE instance_classifications;"
+ sql "DROP TABLE IF EXISTS instance_classifications;"
echo " Dropping instance_sfs"
- sql "DROP TABLE instance_sfs;"
+ sql "DROP TABLE IF EXISTS instance_sfs;"
echo " Dropping instance_sfis"
- sql "DROP TABLE instance_sfis;"
+ sql "DROP TABLE IF EXISTS instance_sfis;"
echo " Dropping sce_classifier_matches"
echo " [Undo adding the VNFFG-SFC instance mapping tables]"
- sql "DROP TABLE sce_classifier_matches;"
+ sql "DROP TABLE IF EXISTS sce_classifier_matches;"
echo " Dropping sce_classifiers"
- sql "DROP TABLE sce_classifiers;"
+ sql "DROP TABLE IF EXISTS sce_classifiers;"
echo " Dropping sce_rsp_hops"
- sql "DROP TABLE sce_rsp_hops;"
+ sql "DROP TABLE IF EXISTS sce_rsp_hops;"
echo " Dropping sce_rsps"
- sql "DROP TABLE sce_rsps;"
+ sql "DROP TABLE IF EXISTS sce_rsps;"
echo " Dropping sce_vnffgs"
- sql "DROP TABLE sce_vnffgs;"
+ sql "DROP TABLE IF EXISTS sce_vnffgs;"
echo " [Altering vim_actions table]"
sql "ALTER TABLE vim_actions MODIFY COLUMN item ENUM('datacenters_flavors','datacenter_images','instance_nets','instance_vms','instance_interfaces') NOT NULL COMMENT 'table where the item is stored'"
sql "DELETE FROM schema_version WHERE version_int='28';"
@@ -1316,6 +1318,19 @@
echo " Change back 'datacenter_nets'"
sql "ALTER TABLE datacenter_nets DROP COLUMN vim_tenant_id, DROP INDEX name_datacenter_id, ADD UNIQUE INDEX name_datacenter_id (name, datacenter_id);"
}
+
+function upgrade_to_34() {
+ echo " Create databases required for WIM features"
+ script="$(find "${DBUTILS}/migrations/up" -iname "34*.sql" | tail -1)"
+ sql "source ${script}"
+}
+
+function downgrade_from_34() {
+ echo " Drop databases required for WIM features"
+ script="$(find "${DBUTILS}/migrations/down" -iname "34*.sql" | tail -1)"
+ sql "source ${script}"
+}
+
#TODO ... put functions here
# echo "db version = "${DATABASE_VER_NUM}
diff --git a/database_utils/migrations/down/34_remove_wim_tables.sql b/database_utils/migrations/down/34_remove_wim_tables.sql
new file mode 100644
index 0000000..c6fa0b4
--- /dev/null
+++ b/database_utils/migrations/down/34_remove_wim_tables.sql
@@ -0,0 +1,27 @@
+--
+-- Tear down database structure required for integrating OSM with
+-- Wide Are Network Infrastructure Managers
+--
+
+DROP TABLE IF EXISTS wim_port_mappings;
+DROP TABLE IF EXISTS wim_nfvo_tenants;
+DROP TABLE IF EXISTS instance_wim_nets;
+
+ALTER TABLE `vim_wim_actions` DROP FOREIGN KEY `FK_actions_wims`;
+ALTER TABLE `vim_wim_actions` DROP INDEX `FK_actions_wims`;
+ALTER TABLE `vim_wim_actions` DROP INDEX `item_type_id`;
+ALTER TABLE `vim_wim_actions` MODIFY `item` enum(
+ 'datacenters_flavors',
+ 'datacenter_images',
+ 'instance_nets',
+ 'instance_vms',
+ 'instance_interfaces') NOT NULL
+ COMMENT 'table where the item is stored';
+ALTER TABLE `vim_wim_actions` MODIFY `datacenter_vim_id` varchar(36) NOT NULL;
+ALTER TABLE `vim_wim_actions` DROP `wim_internal_id`, DROP `wim_account_id`;
+ALTER TABLE `vim_wim_actions` RENAME TO `vim_actions`;
+
+DROP TABLE IF EXISTS wim_accounts;
+DROP TABLE IF EXISTS wims;
+
+DELETE FROM schema_version WHERE version_int='34';
diff --git a/database_utils/migrations/up/34_add_wim_tables.sql b/database_utils/migrations/up/34_add_wim_tables.sql
new file mode 100644
index 0000000..6c6fc33
--- /dev/null
+++ b/database_utils/migrations/up/34_add_wim_tables.sql
@@ -0,0 +1,165 @@
+--
+-- Setup database structure required for integrating OSM with
+-- Wide Are Network Infrastructure Managers
+--
+
+DROP TABLE IF EXISTS wims;
+CREATE TABLE wims (
+ `uuid` varchar(36) NOT NULL,
+ `name` varchar(255) NOT NULL,
+ `description` varchar(255) DEFAULT NULL,
+ `type` varchar(36) NOT NULL DEFAULT 'odl',
+ `wim_url` varchar(150) NOT NULL,
+ `config` varchar(4000) DEFAULT NULL,
+ `created_at` double NOT NULL,
+ `modified_at` double DEFAULT NULL,
+ PRIMARY KEY (`uuid`),
+ UNIQUE KEY `name` (`name`)
+)
+ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT
+COMMENT='WIMs managed by the NFVO.';
+
+DROP TABLE IF EXISTS wim_accounts;
+CREATE TABLE wim_accounts (
+ `uuid` varchar(36) NOT NULL,
+ `name` varchar(255) DEFAULT NULL,
+ `wim_id` varchar(36) NOT NULL,
+ `created` enum('true','false') NOT NULL DEFAULT 'false',
+ `user` varchar(64) DEFAULT NULL,
+ `password` varchar(64) DEFAULT NULL,
+ `config` varchar(4000) DEFAULT NULL,
+ `created_at` double NOT NULL,
+ `modified_at` double DEFAULT NULL,
+ PRIMARY KEY (`uuid`),
+ UNIQUE KEY `wim_name` (`wim_id`,`name`),
+ KEY `FK_wim_accounts_wims` (`wim_id`),
+ CONSTRAINT `FK_wim_accounts_wims` FOREIGN KEY (`wim_id`)
+ REFERENCES `wims` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
+)
+ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT
+COMMENT='WIM accounts by the user';
+
+DROP TABLE IF EXISTS `wim_nfvo_tenants`;
+CREATE TABLE `wim_nfvo_tenants` (
+ `id` integer NOT NULL AUTO_INCREMENT,
+ `nfvo_tenant_id` varchar(36) NOT NULL,
+ `wim_id` varchar(36) NOT NULL,
+ `wim_account_id` varchar(36) NOT NULL,
+ `created_at` double NOT NULL,
+ `modified_at` double DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `wim_nfvo_tenant` (`wim_id`,`nfvo_tenant_id`),
+ KEY `FK_wims_nfvo_tenants` (`wim_id`),
+ KEY `FK_wim_accounts_nfvo_tenants` (`wim_account_id`),
+ KEY `FK_nfvo_tenants_wim_accounts` (`nfvo_tenant_id`),
+ CONSTRAINT `FK_wims_nfvo_tenants` FOREIGN KEY (`wim_id`)
+ REFERENCES `wims` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT `FK_wim_accounts_nfvo_tenants` FOREIGN KEY (`wim_account_id`)
+ REFERENCES `wim_accounts` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT `FK_nfvo_tenants_wim_accounts` FOREIGN KEY (`nfvo_tenant_id`)
+ REFERENCES `nfvo_tenants` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
+)
+ENGINE=InnoDB AUTO_INCREMENT=86 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT
+COMMENT='WIM accounts mapping to NFVO tenants';
+
+DROP TABLE IF EXISTS `instance_wim_nets`;
+CREATE TABLE `instance_wim_nets` (
+ `uuid` varchar(36) NOT NULL,
+ `wim_internal_id` varchar(128) DEFAULT NULL
+ COMMENT 'Internal ID used by the WIM to refer to the network',
+ `instance_scenario_id` varchar(36) DEFAULT NULL,
+ `sce_net_id` varchar(36) DEFAULT NULL,
+ `wim_id` varchar(36) DEFAULT NULL,
+ `wim_account_id` varchar(36) NOT NULL,
+ `status` enum(
+ 'ACTIVE',
+ 'INACTIVE',
+ 'DOWN',
+ 'BUILD',
+ 'ERROR',
+ 'WIM_ERROR',
+ 'DELETED',
+ 'SCHEDULED_CREATION',
+ 'SCHEDULED_DELETION') NOT NULL DEFAULT 'BUILD',
+ `error_msg` varchar(1024) DEFAULT NULL,
+ `wim_info` text,
+ `multipoint` enum('true','false') NOT NULL DEFAULT 'false',
+ `created` enum('true','false') NOT NULL DEFAULT 'false'
+ COMMENT 'Created or already exists at WIM',
+ `created_at` double NOT NULL,
+ `modified_at` double DEFAULT NULL,
+ PRIMARY KEY (`uuid`),
+ KEY `FK_instance_wim_nets_instance_scenarios` (`instance_scenario_id`),
+ KEY `FK_instance_wim_nets_sce_nets` (`sce_net_id`),
+ KEY `FK_instance_wim_nets_wims` (`wim_id`),
+ KEY `FK_instance_wim_nets_wim_accounts` (`wim_account_id`),
+ CONSTRAINT `FK_instance_wim_nets_wim_accounts`
+ FOREIGN KEY (`wim_account_id`) REFERENCES `wim_accounts` (`uuid`),
+ CONSTRAINT `FK_instance_wim_nets_wims`
+ FOREIGN KEY (`wim_id`) REFERENCES `wims` (`uuid`),
+ CONSTRAINT `FK_instance_wim_nets_instance_scenarios`
+ FOREIGN KEY (`instance_scenario_id`) REFERENCES `instance_scenarios` (`uuid`)
+ ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT `FK_instance_wim_nets_sce_nets`
+ FOREIGN KEY (`sce_net_id`) REFERENCES `sce_nets` (`uuid`)
+ ON DELETE SET NULL ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT
+ COMMENT='Instances of wim networks';
+
+ALTER TABLE `vim_actions`
+ RENAME TO `vim_wim_actions`;
+ALTER TABLE `vim_wim_actions`
+ ADD `wim_account_id` varchar(36) DEFAULT NULL AFTER `vim_id`,
+ ADD `wim_internal_id` varchar(64) DEFAULT NULL AFTER `wim_account_id`,
+ MODIFY `datacenter_vim_id` varchar(36) DEFAULT NULL,
+ MODIFY `item` enum(
+ 'datacenters_flavors',
+ 'datacenter_images',
+ 'instance_nets',
+ 'instance_vms',
+ 'instance_interfaces',
+ 'instance_wim_nets') NOT NULL
+ COMMENT 'table where the item is stored';
+ALTER TABLE `vim_wim_actions`
+ ADD INDEX `item_type_id` (`item`, `item_id`);
+ALTER TABLE `vim_wim_actions`
+ ADD INDEX `FK_actions_wims` (`wim_account_id`);
+ALTER TABLE `vim_wim_actions`
+ ADD CONSTRAINT `FK_actions_wims` FOREIGN KEY (`wim_account_id`)
+ REFERENCES `wim_accounts` (`uuid`)
+ ON UPDATE CASCADE ON DELETE CASCADE;
+
+DROP TABLE IF EXISTS `wim_port_mappings`;
+CREATE TABLE `wim_port_mappings` (
+ `id` integer NOT NULL AUTO_INCREMENT,
+ `wim_id` varchar(36) NOT NULL,
+ `datacenter_id` varchar(36) NOT NULL,
+ `pop_switch_dpid` varchar(64) NOT NULL,
+ `pop_switch_port` varchar(64) NOT NULL,
+ `wan_service_endpoint_id` varchar(256) NOT NULL
+ COMMENT 'In case the WIM plugin relies on the wan_service_mapping_info'
+ COMMENT 'this field contains a unique identifier used to check the mapping_info consistency',
+ /* In other words: wan_service_endpoint_id = f(wan_service_mapping_info)
+ * where f is a injective function'
+ */
+ `wan_service_mapping_info` text,
+ `created_at` double NOT NULL,
+ `modified_at` double DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `unique_datacenter_port_mapping`
+ (`datacenter_id`, `pop_switch_dpid`, `pop_switch_port`),
+ UNIQUE KEY `unique_wim_port_mapping`
+ (`wim_id`, `wan_service_endpoint_id`),
+ KEY `FK_wims_wim_physical_connections` (`wim_id`),
+ KEY `FK_datacenters_wim_port_mappings` (`datacenter_id`),
+ CONSTRAINT `FK_wims_wim_port_mappings` FOREIGN KEY (`wim_id`)
+ REFERENCES `wims` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT `FK_datacenters_wim_port_mappings` FOREIGN KEY (`datacenter_id`)
+ REFERENCES `datacenters` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
+)
+ENGINE=InnoDB DEFAULT CHARSET=utf8
+COMMENT='WIM port mappings managed by the WIM.';
+
+-- Update Schema with DB version
+INSERT INTO schema_version
+VALUES (34, '0.34', '0.6.00', 'Added WIM tables', '2018-09-10');
diff --git a/docker/Dockerfile-local b/docker/Dockerfile-local
index 5cc96bb..e7e05ce 100644
--- a/docker/Dockerfile-local
+++ b/docker/Dockerfile-local
@@ -13,6 +13,7 @@
DEBIAN_FRONTEND=noninteractive apt-get -y install python-novaclient python-keystoneclient python-glanceclient python-cinderclient python-neutronclient && \
DEBIAN_FRONTEND=noninteractive apt-get -y install python-cffi libmysqlclient-dev libssl-dev libffi-dev python-mysqldb && \
DEBIAN_FRONTEND=noninteractive apt-get -y install python-openstacksdk python-openstackclient && \
+ DEBIAN_FRONTEND=noninteractive apt-get -y install python-networkx && \
DEBIAN_FRONTEND=noninteractive pip2 install untangle && \
DEBIAN_FRONTEND=noninteractive pip2 install -e git+https://github.com/python-oca/python-oca#egg=oca && \
DEBIAN_FRONTEND=noninteractive apt-get -y install mysql-client
diff --git a/docker/tests.dockerfile b/docker/tests.dockerfile
new file mode 100644
index 0000000..fd5a45c
--- /dev/null
+++ b/docker/tests.dockerfile
@@ -0,0 +1,13 @@
+from ubuntu:xenial
+
+VOLUME /opt/openmano
+VOLUME /var/log/osm
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+
+RUN apt-get update && \
+ apt-get -y install python python-pip mysql-client libmysqlclient-dev && \
+ pip install tox
+
+ENTRYPOINT ["tox"]
diff --git a/docker/tests.yml b/docker/tests.yml
new file mode 100644
index 0000000..a33f0d4
--- /dev/null
+++ b/docker/tests.yml
@@ -0,0 +1,37 @@
+# This file is intended to be used by the developer in the local machine
+# in order to run the tests in isolation
+# To do so, cd into osm_ro and run:
+# docker-compose -f ../docker/tests.yml run --rm tox -c <folder to be tested, eg. wim>
+version: '2'
+services:
+ test-db:
+ image: mysql:5
+ container_name: test-db
+ restart: always
+ environment:
+ - MYSQL_ROOT_PASSWORD=osm4u
+ - MYSQL_USER=mano
+ - MYSQL_PASSWORD=manopw
+ - MYSQL_DATABASE=mano_db
+ tox:
+ container_name: tox
+ depends_on:
+ - test-db
+ build:
+ context: ../
+ dockerfile: docker/tests.dockerfile
+ restart: always
+ environment:
+ - RO_DB_ROOT_PASSWORD=osm4u
+ - TEST_DB_HOST=test-db
+ - TEST_DB_USER=mano
+ - TEST_DB_PASSWORD=manopw
+ - TEST_DB_DATABASE=mano_db
+ ports:
+ - "9090:9090"
+ volumes:
+ - ..:/opt/openmano
+ - /tmp/osm/openmano/logs:/var/log/osm
+ entrypoint:
+ - tox
+ working_dir: /opt/openmano/osm_ro
diff --git a/openmano b/openmano
index 357b91a..1577656 100755
--- a/openmano
+++ b/openmano
@@ -24,7 +24,7 @@
##
"""
-openmano client used to interact with openmano-server (openmanod)
+openmano client used to interact with openmano-server (openmanod)
"""
__author__ = "Alfonso Tierno, Gerardo Garcia, Pablo Montes"
__date__ = "$09-oct-2014 09:09:48$"
@@ -65,6 +65,10 @@
mano_tenant_name = "None"
mano_datacenter_id = "None"
mano_datacenter_name = "None"
+ # WIM additions
+ logger.debug("resolving WIM names")
+ mano_wim_id = "None"
+ mano_wim_name = "None"
try:
mano_tenant_id = _get_item_uuid("tenants", mano_tenant)
URLrequest = "http://%s:%s/openmano/tenants/%s" %(mano_host, mano_port, mano_tenant_id)
@@ -79,17 +83,35 @@
if "error" not in content:
mano_datacenter_id = content["datacenter"]["uuid"]
mano_datacenter_name = content["datacenter"]["name"]
+
+ # WIM
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s" % (
+ mano_host, mano_port, mano_tenant_id, mano_wim)
+ mano_response = requests.get(URLrequest)
+ logger.debug("openmano response: %s", mano_response.text)
+ content = mano_response.json()
+ if "error" not in content:
+ mano_wim_id = content["wim"]["uuid"]
+ mano_wim_name = content["wim"]["name"]
+
except OpenmanoCLIError:
pass
print "OPENMANO_TENANT: %s" %mano_tenant
print " Id: %s" %mano_tenant_id
- print " Name: %s" %mano_tenant_name
+ print " Name: %s" %mano_tenant_name
print "OPENMANO_DATACENTER: %s" %str (mano_datacenter)
print " Id: %s" %mano_datacenter_id
- print " Name: %s" %mano_datacenter_name
+ print " Name: %s" %mano_datacenter_name
+ # WIM
+ print "OPENMANO_WIM: %s" %str (mano_wim)
+ print " Id: %s" %mano_wim_id
+ print " Name: %s" %mano_wim_name
+
else:
print "OPENMANO_TENANT: %s" %mano_tenant
print "OPENMANO_DATACENTER: %s" %str (mano_datacenter)
+ # WIM
+ print "OPENMANO_WIM: %s" %str (mano_wim)
def _print_verbose(mano_response, verbose_level=0):
content = mano_response.json()
@@ -98,7 +120,7 @@
#print "Non expected format output"
print str(content)
return result
-
+
val=content.values()[0]
if type(val)==str:
print val
@@ -111,7 +133,7 @@
#print "Non expected dict/list format output"
print str(content)
return result
-
+
#print content_list
if verbose_level==None:
verbose_level=0
@@ -163,7 +185,7 @@
f.close()
except Exception as e:
return (False, str(e))
-
+
#Read and parse file
if file_name[-5:]=='.yaml' or file_name[-4:]=='.yml' or (file_name[-5:]!='.json' and '\t' not in text):
try:
@@ -176,7 +198,7 @@
return (False, "Error loading file '"+file_name+"' yaml format error" + error_pos)
else: #json
try:
- config = json.loads(text)
+ config = json.loads(text)
except Exception as e:
return (False, "Error loading file '"+file_name+"' json format error " + str(e) )
@@ -234,7 +256,7 @@
elif found > 1:
raise OpenmanoCLIError("%d %s found with name '%s'. uuid must be used" %(found, item, item_name_id))
return uuid
-#
+#
# def check_valid_uuid(uuid):
# id_schema = {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"}
# try:
@@ -242,7 +264,7 @@
# return True
# except js_e.ValidationError:
# return False
-
+
def _get_tenant(tenant_name_id = None):
if not tenant_name_id:
tenant_name_id = mano_tenant
@@ -257,6 +279,14 @@
raise OpenmanoCLIError("neither 'OPENMANO_DATACENTER' environment variable is set nor --datacenter option is used")
return _get_item_uuid("datacenters", datacenter_name_id, tenant)
+# WIM
+def _get_wim(wim_name_id = None, tenant = "any"):
+ if not wim_name_id:
+ wim_name_id = mano_wim
+ if not wim_name_id:
+ raise OpenmanoCLIError("neither 'OPENMANO_WIM' environment variable is set nor --wim option is used")
+ return _get_item_uuid("wims", wim_name_id, tenant)
+
def vnf_create(args):
#print "vnf-create",args
headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
@@ -357,11 +387,11 @@
elif str(e) == 'image checksum': error_pos= "missing field 'vnf':'VNFC'['image checksum']"
else: error_pos="wrong format"
print "Wrong VNF descriptor: " + error_pos
- return -1
+ return -1
payload_req = json.dumps(myvnf)
-
+
#print payload_req
-
+
URLrequest = "http://{}:{}/openmano{}/{}/{token}".format(mano_host, mano_port, api_version, tenant, token=token)
logger.debug("openmano request: %s", payload_req)
mano_response = requests.post(URLrequest, headers = headers_req, data=payload_req)
@@ -486,7 +516,7 @@
nsd['description'] = args.description
payload_req = yaml.safe_dump(myscenario, explicit_start=True, indent=4, default_flow_style=False, tags=False,
encoding='utf-8', allow_unicode=True)
-
+
# print payload_req
URLrequest = "http://{host}:{port}/openmano{api}/{tenant}/{token}".format(
host=mano_host, port=mano_port, api=api_version, tenant=tenant, token=token)
@@ -607,26 +637,26 @@
# action[actionCmd]["datacenter"] = args.datacenter
# elif mano_datacenter != None:
# action[actionCmd]["datacenter"] = mano_datacenter
-#
+#
# if args.description:
# action[actionCmd]["description"] = args.description
# payload_req = json.dumps(action, indent=4)
# #print payload_req
-#
+#
# URLrequest = "http://%s:%s/openmano/%s/scenarios/%s/action" %(mano_host, mano_port, mano_tenant, args.scenario)
# logger.debug("openmano request: %s", payload_req)
# mano_response = requests.post(URLrequest, headers = headers_req, data=payload_req)
# logger.debug("openmano response: %s", mano_response.text )
# if args.verbose==None:
# args.verbose=0
-#
+#
# result = 0 if mano_response.status_code==200 else mano_response.status_code
# content = mano_response.json()
# #print json.dumps(content, indent=4)
# if args.verbose >= 3:
# print yaml.safe_dump(content, indent=4, default_flow_style=False)
# return result
-#
+#
# if mano_response.status_code == 200:
# myoutput = "%s %s" %(content['uuid'].ljust(38),content['name'].ljust(20))
# if args.verbose >=1:
@@ -655,7 +685,7 @@
logger.debug("openmano request: %s", payload_req)
mano_response = requests.post(URLrequest, headers = headers_req, data=payload_req)
logger.debug("openmano response: %s", mano_response.text )
-
+
result = 0 if mano_response.status_code==200 else mano_response.status_code
content = mano_response.json()
#print json.dumps(content, indent=4)
@@ -708,7 +738,7 @@
net_scenario = net_tuple[0].strip()
net_datacenter = net_tuple[1].strip()
if net_scenario not in myInstance["instance"]["networks"]:
- myInstance["instance"]["networks"][net_scenario] = {}
+ myInstance["instance"]["networks"][net_scenario] = {}
if "sites" not in myInstance["instance"]["networks"][net_scenario]:
myInstance["instance"]["networks"][net_scenario]["sites"] = [ {} ]
myInstance["instance"]["networks"][net_scenario]["sites"][0]["netmap-use"] = net_datacenter
@@ -729,7 +759,7 @@
print "error at netmap-create. Expected net-scenario=net-datacenter or net-scenario. (%s)?" % net_comma
return
if net_scenario not in myInstance["instance"]["networks"]:
- myInstance["instance"]["networks"][net_scenario] = {}
+ myInstance["instance"]["networks"][net_scenario] = {}
if "sites" not in myInstance["instance"]["networks"][net_scenario]:
myInstance["instance"]["networks"][net_scenario]["sites"] = [ {} ]
myInstance["instance"]["networks"][net_scenario]["sites"][0]["netmap-create"] = net_datacenter
@@ -766,7 +796,7 @@
except Exception as e:
print "Cannot obtain any public ssh key. Error '{}'. Try not using --keymap-auto".format(str(e))
return 1
-
+
if "cloud-config" not in myInstance["instance"]:
myInstance["instance"]["cloud-config"] = {}
cloud_config = myInstance["instance"]["cloud-config"]
@@ -775,8 +805,8 @@
if user:
if "users" not in cloud_config:
cloud_config["users"] = []
- cloud_config["users"].append({"name": user, "key-pairs": keys })
-
+ cloud_config["users"].append({"name": user, "key-pairs": keys })
+
payload_req = yaml.safe_dump(myInstance, explicit_start=True, indent=4, default_flow_style=False, tags=False, encoding='utf-8', allow_unicode=True)
logger.debug("openmano request: %s", payload_req)
URLrequest = "http://%s:%s/openmano/%s/instances" %(mano_host, mano_port, tenant)
@@ -784,7 +814,7 @@
logger.debug("openmano response: %s", mano_response.text )
if args.verbose==None:
args.verbose=0
-
+
result = 0 if mano_response.status_code==200 else mano_response.status_code
content = mano_response.json()
#print json.dumps(content, indent=4)
@@ -933,7 +963,7 @@
action["vnfs"] = args.vnf
if args.vm:
action["vms"] = args.vm
-
+
headers_req = {'content-type': 'application/json'}
payload_req = json.dumps(action, indent=4)
URLrequest = "http://%s:%s/openmano/%s/instances/%s/action" %(mano_host, mano_port, tenant, toact)
@@ -969,11 +999,11 @@
headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
tenant_dict={"name": args.name}
if args.description!=None:
- tenant_dict["description"] = args.description
+ tenant_dict["description"] = args.description
payload_req = json.dumps( {"tenant": tenant_dict })
-
+
#print payload_req
-
+
URLrequest = "http://%s:%s/openmano/tenants" %(mano_host, mano_port)
logger.debug("openmano request: %s", payload_req)
mano_response = requests.post(URLrequest, headers=headers_req, data=payload_req)
@@ -1018,7 +1048,7 @@
tenant = _get_tenant()
datacenter = _get_datacenter(args.name)
headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
-
+
datacenter_dict={}
if args.vim_tenant_id != None:
datacenter_dict['vim_tenant'] = args.vim_tenant_id
@@ -1034,7 +1064,7 @@
payload_req = json.dumps( {"datacenter": datacenter_dict })
#print payload_req
-
+
URLrequest = "http://%s:%s/openmano/%s/datacenters/%s" %(mano_host, mano_port, tenant, datacenter)
logger.debug("openmano request: %s", payload_req)
mano_response = requests.post(URLrequest, headers=headers_req, data=payload_req)
@@ -1103,11 +1133,11 @@
headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
datacenter_dict={"name": args.name, "vim_url": args.url}
if args.description!=None:
- datacenter_dict["description"] = args.description
+ datacenter_dict["description"] = args.description
if args.type!=None:
- datacenter_dict["type"] = args.type
+ datacenter_dict["type"] = args.type
if args.url!=None:
- datacenter_dict["vim_url_admin"] = args.url_admin
+ datacenter_dict["vim_url_admin"] = args.url_admin
if args.config!=None:
datacenter_dict["config"] = _load_file_or_yaml(args.config)
if args.sdn_controller!=None:
@@ -1117,9 +1147,9 @@
datacenter_dict['config'] = {}
datacenter_dict['config']['sdn-controller'] = sdn_controller
payload_req = json.dumps( {"datacenter": datacenter_dict })
-
+
#print payload_req
-
+
URLrequest = "http://%s:%s/openmano/datacenters" %(mano_host, mano_port)
logger.debug("openmano request: %s", payload_req)
mano_response = requests.post(URLrequest, headers=headers_req, data=payload_req)
@@ -1149,9 +1179,9 @@
def datacenter_list(args):
#print "datacenter-list",args
tenant='any' if args.all else _get_tenant()
-
+
if args.name:
- toshow = _get_item_uuid("datacenters", args.name, tenant)
+ toshow = _get_item_uuid("datacenters", args.name, tenant)
URLrequest = "http://%s:%s/openmano/%s/datacenters/%s" %(mano_host, mano_port, tenant, toshow)
else:
URLrequest = "http://%s:%s/openmano/%s/datacenters" %(mano_host, mano_port, tenant)
@@ -1502,7 +1532,7 @@
elif args.action == "net-delete":
args.netmap = args.net
args.all = False
-
+
args.action = "netmap" + args.action[3:]
args.vim_name=None
args.vim_id=None
@@ -1519,13 +1549,13 @@
args.verbose=0
headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
URLrequest = "http://%s:%s/openmano/%s/datacenters/%s/netmaps" %(mano_host, mano_port, tenant, datacenter)
-
+
if args.action=="netmap-list":
if args.netmap:
URLrequest += "/" + args.netmap
args.verbose += 1
mano_response = requests.get(URLrequest)
-
+
elif args.action=="netmap-delete":
if args.netmap and args.all:
print "you can not use a netmap name and the option --all at the same time"
@@ -1533,7 +1563,7 @@
if args.netmap:
force_text= "Delete default netmap '%s' from datacenter '%s' (y/N)? " % (args.netmap, datacenter)
URLrequest += "/" + args.netmap
- elif args.all:
+ elif args.all:
force_text="Delete all default netmaps from datacenter '%s' (y/N)? " % (datacenter)
else:
print "you must specify a netmap name or the option --all"
@@ -1569,7 +1599,7 @@
payload["netmap"]["vim_name"] = args.vim_name
payload_req = json.dumps(payload)
logger.debug("openmano request: %s", payload_req)
-
+
if args.action=="netmap-edit" and not args.force:
if len(payload["netmap"]) == 0:
print "You must supply some parameter to edit"
@@ -1599,7 +1629,7 @@
if args.element[:-1] not in payload:
payload = {args.element[:-1]: payload }
payload_req = json.dumps(payload)
-
+
#print payload_req
if not args.force or (args.name==None and args.filer==None):
r = raw_input(" Edit " + args.element[:-1] + " " + args.name + " (y/N)? ")
@@ -1665,6 +1695,260 @@
return _print_verbose(mano_response, args.verbose)
+# WIM
+def wim_account_create(args):
+ tenant = _get_tenant()
+ wim = _get_wim(args.name)
+ headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
+
+ wim_dict = {}
+ if args.account_name is not None:
+ wim_dict['name'] = args.account_name
+ if args.user is not None:
+ wim_dict['user'] = args.user
+ if args.password is not None:
+ wim_dict['password'] = args.password
+ if args.config is not None:
+ wim_dict["config"] = _load_file_or_yaml(args.config)
+
+ payload_req = json.dumps({"wim_account": wim_dict})
+
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s" % (mano_host, mano_port, tenant, wim)
+ logger.debug("openmano request: %s", payload_req)
+ mano_response = requests.post(URLrequest, headers=headers_req, data=payload_req)
+ logger.debug("openmano response: %s", mano_response.text)
+ result = _print_verbose(mano_response, args.verbose)
+ # provide addional information if error
+ if mano_response.status_code != 200:
+ content = mano_response.json()
+ if "already in use for 'name'" in content['error']['description'] and \
+ "to database wim_tenants table" in content['error']['description']:
+ print "Try to specify a different name with --wim-tenant-name"
+ return result
+
+
+def wim_account_delete(args):
+ if args.all:
+ tenant = "any"
+ else:
+ tenant = _get_tenant()
+ wim = _get_wim(args.name, tenant)
+ headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s" % (mano_host, mano_port, tenant, wim)
+ mano_response = requests.delete(URLrequest, headers=headers_req)
+ logger.debug("openmano response: %s", mano_response.text)
+ content = mano_response.json()
+ # print json.dumps(content, indent=4)
+ result = 0 if mano_response.status_code == 200 else mano_response.status_code
+ if mano_response.status_code == 200:
+ print content['result']
+ else:
+ print content['error']['description']
+ return result
+
+
+def wim_account_edit(args):
+ tenant = _get_tenant()
+ wim = _get_wim(args.name)
+ headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
+
+ wim_dict = {}
+ if not args.account_name:
+ wim_dict['name'] = args.vim_tenant_name
+ if not args.user:
+ wim_dict['user'] = args.user
+ if not args.password:
+ wim_dict['password'] = args.password
+ if not args.config:
+ wim_dict["config"] = _load_file_or_yaml(args.config)
+
+ payload_req = json.dumps({"wim_account": wim_dict})
+
+ # print payload_req
+
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s" % (mano_host, mano_port, tenant, wim)
+ logger.debug("openmano request: %s", payload_req)
+ mano_response = requests.post(URLrequest, headers=headers_req, data=payload_req)
+ logger.debug("openmano response: %s", mano_response.text)
+ result = _print_verbose(mano_response, args.verbose)
+ # provide addional information if error
+ if mano_response.status_code != 200:
+ content = mano_response.json()
+ if "already in use for 'name'" in content['error']['description'] and \
+ "to database wim_tenants table" in content['error']['description']:
+ print "Try to specify a different name with --wim-tenant-name"
+ return result
+
+def wim_create(args):
+ headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
+ wim_dict = {"name": args.name, "wim_url": args.url}
+ if args.description != None:
+ wim_dict["description"] = args.description
+ if args.type != None:
+ wim_dict["type"] = args.type
+ if args.config != None:
+ wim_dict["config"] = _load_file_or_yaml(args.config)
+
+ payload_req = json.dumps({"wim": wim_dict})
+
+ URLrequest = "http://%s:%s/openmano/wims" % (mano_host, mano_port)
+ logger.debug("openmano request: %s", payload_req)
+ mano_response = requests.post(URLrequest, headers=headers_req, data=payload_req)
+ logger.debug("openmano response: %s", mano_response.text)
+ return _print_verbose(mano_response, args.verbose)
+
+
+def wim_edit(args):
+ tenant = _get_tenant()
+ element = _get_item_uuid('wims', args.name, tenant)
+ headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
+ URLrequest = "http://%s:%s/openmano/wims/%s" % (mano_host, mano_port, element)
+
+ has_arguments = False
+ if args.file != None:
+ has_arguments = True
+ payload = _load_file_or_yaml(args.file)
+ else:
+ payload = {}
+
+ if not has_arguments:
+ raise OpenmanoCLIError("At least one argument must be provided to modify the wim")
+
+ if 'wim' not in payload:
+ payload = {'wim': payload}
+ payload_req = json.dumps(payload)
+
+ # print payload_req
+ if not args.force or (args.name == None and args.filer == None):
+ r = raw_input(" Edit wim " + args.name + " (y/N)? ")
+ if len(r) > 0 and r[0].lower() == "y":
+ pass
+ else:
+ return 0
+ logger.debug("openmano request: %s", payload_req)
+ mano_response = requests.put(URLrequest, headers=headers_req, data=payload_req)
+ logger.debug("openmano response: %s", mano_response.text)
+ if args.verbose == None:
+ args.verbose = 0
+ if args.name != None:
+ args.verbose += 1
+ return _print_verbose(mano_response, args.verbose)
+
+
+def wim_delete(args):
+ # print "wim-delete",args
+ todelete = _get_item_uuid("wims", args.name, "any")
+ if not args.force:
+ r = raw_input("Delete wim %s (y/N)? " % (args.name))
+ if not (len(r) > 0 and r[0].lower() == "y"):
+ return 0
+ URLrequest = "http://%s:%s/openmano/wims/%s" % (mano_host, mano_port, todelete)
+ mano_response = requests.delete(URLrequest)
+ logger.debug("openmano response: %s", mano_response.text)
+ result = 0 if mano_response.status_code == 200 else mano_response.status_code
+ content = mano_response.json()
+ # print json.dumps(content, indent=4)
+ if mano_response.status_code == 200:
+ print content['result']
+ else:
+ print content['error']['description']
+ return result
+
+
+def wim_list(args):
+ # print "wim-list",args
+ tenant = 'any' if args.all else _get_tenant()
+
+ if args.name:
+ toshow = _get_item_uuid("wims", args.name, tenant)
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s" % (mano_host, mano_port, tenant, toshow)
+ else:
+ URLrequest = "http://%s:%s/openmano/%s/wims" % (mano_host, mano_port, tenant)
+ mano_response = requests.get(URLrequest)
+ logger.debug("openmano response: %s", mano_response.text)
+ if args.verbose == None:
+ args.verbose = 0
+ if args.name != None:
+ args.verbose += 1
+ return _print_verbose(mano_response, args.verbose)
+
+
+def wim_port_mapping_set(args):
+ tenant = _get_tenant()
+ wim = _get_wim(args.name, tenant)
+ headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
+
+ if not args.file:
+ raise OpenmanoCLIError(
+ "No yaml/json has been provided specifying the WIM port mapping")
+ wim_port_mapping = _load_file_or_yaml(args.file)
+
+ payload_req = json.dumps({"wim_port_mapping": wim_port_mapping})
+
+ # read
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s/port_mapping" % (mano_host, mano_port, tenant, wim)
+ mano_response = requests.get(URLrequest)
+ logger.debug("openmano response: %s", mano_response.text)
+ port_mapping = mano_response.json()
+
+ if mano_response.status_code != 200:
+ str(mano_response.json())
+ raise OpenmanoCLIError("openmano client error: {}".format(port_mapping['error']['description']))
+ # TODO: check this if statement
+ if len(port_mapping["wim_port_mapping"]) > 0:
+ if not args.force:
+ r = raw_input("WIM %s already contains a port mapping. Overwrite? (y/N)? " % (wim))
+ if not (len(r) > 0 and r[0].lower() == "y"):
+ return 0
+
+ # clear
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s/port_mapping" % (mano_host, mano_port, tenant, wim)
+ mano_response = requests.delete(URLrequest)
+ logger.debug("openmano response: %s", mano_response.text)
+ if mano_response.status_code != 200:
+ return _print_verbose(mano_response, args.verbose)
+
+ # set
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s/port_mapping" % (mano_host, mano_port, tenant, wim)
+ logger.debug("openmano request: %s", payload_req)
+ mano_response = requests.post(URLrequest, headers=headers_req, data=payload_req)
+ logger.debug("openmano response: %s", mano_response.text)
+ return _print_verbose(mano_response, 4)
+
+
+def wim_port_mapping_list(args):
+ tenant = _get_tenant()
+ wim = _get_wim(args.name, tenant)
+
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s/port_mapping" % (mano_host, mano_port, tenant, wim)
+ mano_response = requests.get(URLrequest)
+ logger.debug("openmano response: %s", mano_response.text)
+
+ return _print_verbose(mano_response, 4)
+
+
+def wim_port_mapping_clear(args):
+ tenant = _get_tenant()
+ wim = _get_wim(args.name, tenant)
+
+ if not args.force:
+ r = raw_input("Clear WIM port mapping for wim %s (y/N)? " % (wim))
+ if not (len(r) > 0 and r[0].lower() == "y"):
+ return 0
+
+ URLrequest = "http://%s:%s/openmano/%s/wims/%s/port_mapping" % (mano_host, mano_port, tenant, wim)
+ mano_response = requests.delete(URLrequest)
+ logger.debug("openmano response: %s", mano_response.text)
+ content = mano_response.json()
+ # print json.dumps(content, indent=4)
+ result = 0 if mano_response.status_code == 200 else mano_response.status_code
+ if mano_response.status_code == 200:
+ print content['result']
+ else:
+ print content['error']['description']
+ return result
+
+
def version(args):
headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
URLrequest = "http://%s:%s/openmano/version" % (mano_host, mano_port)
@@ -1679,19 +1963,21 @@
global mano_tenant
if __name__=="__main__":
-
+
mano_tenant = os.getenv('OPENMANO_TENANT', None)
mano_host = os.getenv('OPENMANO_HOST',"localhost")
mano_port = os.getenv('OPENMANO_PORT',"9090")
mano_datacenter = os.getenv('OPENMANO_DATACENTER',None)
-
+ # WIM env variable for default WIM
+ mano_wim = os.getenv('OPENMANO_WIM', None)
+
main_parser = ThrowingArgumentParser(description='User program to interact with OPENMANO-SERVER (openmanod)')
main_parser.add_argument('--version', action='version', help="get version of this client",
version='%(prog)s client version ' + __version__ +
" (Note: use '%(prog)s version' to get server version)")
subparsers = main_parser.add_subparsers(help='commands')
-
+
parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--verbose', '-v', action='count', help="increase verbosity level. Use several times")
parent_parser.add_argument('--debug', '-d', action='store_true', help="show debug information")
@@ -1717,13 +2003,13 @@
vnf_list_parser.add_argument("-a", "--all", action="store_true", help="shows all vnfs, not only the owned or public ones")
#vnf_list_parser.add_argument('--descriptor', help="prints the VNF descriptor", action="store_true")
vnf_list_parser.set_defaults(func=vnf_list)
-
+
vnf_delete_parser = subparsers.add_parser('vnf-delete', parents=[parent_parser], help="deletes a vnf from the catalogue")
vnf_delete_parser.add_argument("name", action="store", help="name or uuid of the VNF to be deleted")
vnf_delete_parser.add_argument("-f", "--force", action="store_true", help="forces deletion without asking")
vnf_delete_parser.add_argument("-a", "--all", action="store_true", help="allow delete not owned or privated one")
vnf_delete_parser.set_defaults(func=vnf_delete)
-
+
scenario_create_parser = subparsers.add_parser('scenario-create', parents=[parent_parser], help="adds a scenario into the OPENMANO DB")
scenario_create_parser.add_argument("file", action="store", help="location of the YAML file describing the scenario").completer = FilesCompleter
scenario_create_parser.add_argument("--name", action="store", help="name of the scenario (if it exists in the YAML scenario, it is overwritten)")
@@ -1735,7 +2021,7 @@
#scenario_list_parser.add_argument('--descriptor', help="prints the scenario descriptor", action="store_true")
scenario_list_parser.add_argument("-a", "--all", action="store_true", help="shows all scenarios, not only the owned or public ones")
scenario_list_parser.set_defaults(func=scenario_list)
-
+
scenario_delete_parser = subparsers.add_parser('scenario-delete', parents=[parent_parser], help="deletes a scenario from the OPENMANO DB")
scenario_delete_parser.add_argument("name", action="store", help="name or uuid of the scenario to be deleted")
scenario_delete_parser.add_argument("-f", "--force", action="store_true", help="forces deletion without asking")
@@ -1749,12 +2035,12 @@
scenario_deploy_parser.add_argument("--datacenter", action="store", help="specifies the datacenter. Needed if several datacenters are available")
scenario_deploy_parser.add_argument("--description", action="store", help="description of the instance")
scenario_deploy_parser.set_defaults(func=scenario_deploy)
-
+
scenario_deploy_parser = subparsers.add_parser('scenario-verify', help="verifies if a scenario can be deployed (deploys it and deletes it)")
scenario_deploy_parser.add_argument("scenario", action="store", help="name or uuid of the scenario to be verified")
scenario_deploy_parser.add_argument('--debug', '-d', action='store_true', help="show debug information")
scenario_deploy_parser.set_defaults(func=scenario_verify)
-
+
instance_scenario_create_parser = subparsers.add_parser('instance-scenario-create', parents=[parent_parser], help="deploys a scenario")
instance_scenario_create_parser.add_argument("file", nargs='?', help="descriptor of the instance. Must be a file or yaml/json text")
instance_scenario_create_parser.add_argument("--scenario", action="store", help="name or uuid of the scenario to be deployed")
@@ -1778,7 +2064,7 @@
instance_scenario_delete_parser.add_argument("-f", "--force", action="store_true", help="forces deletion without asking")
instance_scenario_delete_parser.add_argument("-a", "--all", action="store_true", help="allow delete not owned or privated one")
instance_scenario_delete_parser.set_defaults(func=instance_scenario_delete)
-
+
instance_scenario_action_parser = subparsers.add_parser('instance-scenario-action', parents=[parent_parser], help="invoke an action over part or the whole scenario instance")
instance_scenario_action_parser.add_argument("name", action="store", help="name or uuid of the scenario instance")
instance_scenario_action_parser.add_argument("action", action="store", type=str, \
@@ -1798,7 +2084,7 @@
#instance_scenario_status_parser = subparsers.add_parser('instance-scenario-status', help="show the status of a scenario instance")
#instance_scenario_status_parser.add_argument("name", action="store", help="name or uuid of the scenario instance")
#instance_scenario_status_parser.set_defaults(func=instance_scenario_status)
-
+
tenant_create_parser = subparsers.add_parser('tenant-create', parents=[parent_parser], help="creates a new tenant")
tenant_create_parser.add_argument("name", action="store", help="name for the tenant")
tenant_create_parser.add_argument("--description", action="store", help="description of the tenant")
@@ -1958,6 +2244,128 @@
sdn_controller_delete_parser.set_defaults(func=sdn_controller_delete)
# =======================
+ # WIM ======================= WIM section==================
+
+ # WIM create
+ wim_create_parser = subparsers.add_parser('wim-create',
+ parents=[parent_parser], help="creates a new wim")
+ wim_create_parser.add_argument("name", action="store",
+ help="name for the wim")
+ wim_create_parser.add_argument("url", action="store",
+ help="url for the wim")
+ wim_create_parser.add_argument("--type", action="store",
+ help="wim type: tapi, onos or odl (default)")
+ wim_create_parser.add_argument("--config", action="store",
+ help="additional configuration in json/yaml format")
+ wim_create_parser.add_argument("--description", action="store",
+ help="description of the wim")
+ wim_create_parser.set_defaults(func=wim_create)
+
+ # WIM delete
+ wim_delete_parser = subparsers.add_parser('wim-delete',
+ parents=[parent_parser], help="deletes a wim from the catalogue")
+ wim_delete_parser.add_argument("name", action="store",
+ help="name or uuid of the wim to be deleted")
+ wim_delete_parser.add_argument("-f", "--force", action="store_true",
+ help="forces deletion without asking")
+ wim_delete_parser.set_defaults(func=wim_delete)
+
+ # WIM edit
+ wim_edit_parser = subparsers.add_parser('wim-edit',
+ parents=[parent_parser], help="edits a wim")
+ wim_edit_parser.add_argument("name", help="name or uuid of the wim")
+ wim_edit_parser.add_argument("--file",
+ help="json/yaml text or file with the changes")\
+ .completer = FilesCompleter
+ wim_edit_parser.add_argument("-f", "--force", action="store_true",
+ help="do not prompt for confirmation")
+ wim_edit_parser.set_defaults(func=wim_edit)
+
+ # WIM list
+ wim_list_parser = subparsers.add_parser('wim-list',
+ parents=[parent_parser],
+ help="lists information about registered wims")
+ wim_list_parser.add_argument("name", nargs='?',
+ help="name or uuid of the wim")
+ wim_list_parser.add_argument("-a", "--all", action="store_true",
+ help="shows all wims, not only wims attached to tenant")
+ wim_list_parser.set_defaults(func=wim_list)
+
+ # WIM account create
+ wim_attach_parser = subparsers.add_parser('wim-account-create', parents=
+ [parent_parser], help="associates a wim account to the operating tenant")
+ wim_attach_parser.add_argument("name", help="name or uuid of the wim")
+ wim_attach_parser.add_argument('--account-name', action='store',
+ help="specify a name for the wim account.")
+ wim_attach_parser.add_argument("--user", action="store",
+ help="user credentials for the wim account")
+ wim_attach_parser.add_argument("--password", action="store",
+ help="password credentials for the wim account")
+ wim_attach_parser.add_argument("--config", action="store",
+ help="additional configuration in json/yaml format")
+ wim_attach_parser.set_defaults(func=wim_account_create)
+
+ # WIM account delete
+ wim_detach_parser = subparsers.add_parser('wim-account-delete',
+ parents=[parent_parser],
+ help="removes the association "
+ "between a wim account and the operating tenant")
+ wim_detach_parser.add_argument("name", help="name or uuid of the wim")
+ wim_detach_parser.add_argument("-a", "--all", action="store_true",
+ help="removes all associations from this wim")
+ wim_detach_parser.add_argument("-f", "--force", action="store_true",
+ help="forces delete without asking")
+ wim_detach_parser.set_defaults(func=wim_account_delete)
+
+ # WIM account edit
+ wim_attach_edit_parser = subparsers.add_parser('wim-account-edit', parents=
+ [parent_parser], help="modifies the association of a wim account to the operating tenant")
+ wim_attach_edit_parser.add_argument("name", help="name or uuid of the wim")
+ wim_attach_edit_parser.add_argument('--account-name', action='store',
+ help="specify a name for the wim account.")
+ wim_attach_edit_parser.add_argument("--user", action="store",
+ help="user credentials for the wim account")
+ wim_attach_edit_parser.add_argument("--password", action="store",
+ help="password credentials for the wim account")
+ wim_attach_edit_parser.add_argument("--config", action="store",
+ help="additional configuration in json/yaml format")
+ wim_attach_edit_parser.set_defaults(func=wim_account_edit)
+
+ # WIM port mapping set
+ wim_port_mapping_set_parser = subparsers.add_parser('wim-port-mapping-set',
+ parents=[parent_parser],
+ help="Load a file with the mappings "
+ "of ports of a WAN switch that is "
+ "connected to a PoP and the ports "
+ "of the switch controlled by the PoP")
+ wim_port_mapping_set_parser.add_argument("name", action="store",
+ help="specifies the wim")
+ wim_port_mapping_set_parser.add_argument("file",
+ help="json/yaml text or file with the wim port mapping")\
+ .completer = FilesCompleter
+ wim_port_mapping_set_parser.add_argument("-f", "--force",
+ action="store_true", help="forces overwriting without asking")
+ wim_port_mapping_set_parser.set_defaults(func=wim_port_mapping_set)
+
+ # WIM port mapping list
+ wim_port_mapping_list_parser = subparsers.add_parser('wim-port-mapping-list',
+ parents=[parent_parser], help="Show the port mappings for a wim")
+ wim_port_mapping_list_parser.add_argument("name", action="store",
+ help="specifies the wim")
+ wim_port_mapping_list_parser.set_defaults(func=wim_port_mapping_list)
+
+ # WIM port mapping clear
+ wim_port_mapping_clear_parser = subparsers.add_parser('wim-port-mapping-clear',
+ parents=[parent_parser], help="Clean the port mapping in a wim")
+ wim_port_mapping_clear_parser.add_argument("name", action="store",
+ help="specifies the wim")
+ wim_port_mapping_clear_parser.add_argument("-f", "--force",
+ action="store_true",
+ help="forces clearing without asking")
+ wim_port_mapping_clear_parser.set_defaults(func=wim_port_mapping_clear)
+
+ # =======================================================
+
action_dict={'net-update': 'retrieves external networks from datacenter',
'net-edit': 'edits an external network',
'net-delete': 'deletes an external network',
@@ -2062,7 +2470,7 @@
vim_item_create_parser.set_defaults(func=vim_action, item=item, action="create")
argcomplete.autocomplete(main_parser)
-
+
try:
args = main_parser.parse_args()
#logging info
@@ -2088,7 +2496,7 @@
except OpenmanoCLIError as e:
print str(e)
result = -5
-
+
#print result
exit(result)
diff --git a/openmanod b/openmanod
index c04d66a..5e9cc92 100755
--- a/openmanod
+++ b/openmanod
@@ -27,7 +27,7 @@
Main program that implements a reference NFVO (Network Functions Virtualisation Orchestrator).
It interfaces with an NFV VIM through its API and offers a northbound interface, based on REST (openmano API),
where NFV services are offered including the creation and deletion of VNF templates, VNF instances,
-network service templates and network service instances.
+network service templates and network service instances.
It loads the configuration file and launches the http_server thread that will listen requests using openmano API.
"""
@@ -44,14 +44,15 @@
from osm_ro import httpserver, nfvo, nfvo_db
from osm_ro.openmano_schemas import config_schema
from osm_ro.db_base import db_base_Exception
+from osm_ro.wim.engine import WimEngine
+from osm_ro.wim.persistence import WimPersistence
import osm_ro
__author__ = "Alfonso Tierno, Gerardo Garcia, Pablo Montes"
__date__ = "$26-aug-2014 11:09:29$"
-__version__ = "0.5.84-r594"
+__version__ = "0.6.00"
version_date = "Nov 2018"
-database_version = 33 # expected database schema version
-
+database_version = 34 # expected database schema version
global global_config
global logger
@@ -106,7 +107,7 @@
def console_port_iterator():
- '''this iterator deals with the http_console_ports
+ '''this iterator deals with the http_console_ports
returning the ports one by one
'''
index = 0
@@ -296,7 +297,7 @@
logger.critical("Starting openmano server version: '%s %s' command: '%s'",
__version__, version_date, " ".join(sys.argv))
- for log_module in ("nfvo", "http", "vim", "db", "console", "ovim"):
+ for log_module in ("nfvo", "http", "vim", "wim", "db", "console", "ovim"):
log_level_module = "log_level_" + log_module
log_file_module = "log_file_" + log_module
logger_module = logging.getLogger('openmano.' + log_module)
@@ -343,9 +344,18 @@
pass # if tenant exist (NfvoException error 409), ignore
else: # otherwise print and error and continue
logger.error("Cannot create tenant '{}': {}".format(create_tenant, e))
- nfvo.start_service(mydb)
- httpthread = httpserver.httpserver(mydb, False, global_config['http_host'], global_config['http_port'])
+ # WIM module
+ wim_persistence = WimPersistence(mydb)
+ wim_engine = WimEngine(wim_persistence)
+ # ---
+ nfvo.start_service(mydb, wim_persistence, wim_engine)
+
+ httpthread = httpserver.httpserver(
+ mydb, False,
+ global_config['http_host'], global_config['http_port'],
+ wim_persistence, wim_engine
+ )
httpthread.start()
if 'http_admin_port' in global_config:
diff --git a/osm_ro/db_base.py b/osm_ro/db_base.py
index 2a6cd8c..7b48f43 100644
--- a/osm_ro/db_base.py
+++ b/osm_ro/db_base.py
@@ -37,14 +37,7 @@
import datetime
from jsonschema import validate as js_v, exceptions as js_e
-HTTP_Bad_Request = 400
-HTTP_Unauthorized = 401
-HTTP_Not_Found = 404
-HTTP_Method_Not_Allowed = 405
-HTTP_Request_Timeout = 408
-HTTP_Conflict = 409
-HTTP_Service_Unavailable = 503
-HTTP_Internal_Server_Error = 500
+from .http_tools import errors as httperrors
def _check_valid_uuid(uuid):
id_schema = {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"}
@@ -68,7 +61,7 @@
for k,v in var.items():
if type(v) is datetime.datetime:
var[k]= v.strftime('%Y-%m-%dT%H:%M:%S')
- elif type(v) is dict or type(v) is list or type(v) is tuple:
+ elif type(v) is dict or type(v) is list or type(v) is tuple:
_convert_datetime2str(v)
if len(var) == 0: return True
elif type(var) is list or type(var) is tuple:
@@ -76,7 +69,7 @@
_convert_datetime2str(v)
def _convert_bandwidth(data, reverse=False, logger=None):
- '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
+ '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
It assumes that bandwidth is well formed
Attributes:
'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid
@@ -111,7 +104,7 @@
_convert_bandwidth(k, reverse, logger)
def _convert_str2boolean(data, items):
- '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
+ '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
Done recursively
Attributes:
'data': dictionary variable to be checked. None or empty is considered valid
@@ -135,16 +128,15 @@
if type(k) is dict or type(k) is tuple or type(k) is list:
_convert_str2boolean(k, items)
-class db_base_Exception(Exception):
+class db_base_Exception(httperrors.HttpMappedError):
'''Common Exception for all database exceptions'''
-
- def __init__(self, message, http_code=HTTP_Bad_Request):
- Exception.__init__(self, message)
- self.http_code = http_code
+
+ def __init__(self, message, http_code=httperrors.Bad_Request):
+ super(db_base_Exception, self).__init__(message, http_code)
class db_base():
tables_with_created_field=()
-
+
def __init__(self, host=None, user=None, passwd=None, database=None, log_name='db', log_level=None):
self.host = host
self.user = user
@@ -155,9 +147,9 @@
self.logger = logging.getLogger(log_name)
if self.log_level:
self.logger.setLevel( getattr(logging, log_level) )
-
+
def connect(self, host=None, user=None, passwd=None, database=None):
- '''Connect to specific data base.
+ '''Connect to specific data base.
The first time a valid host, user, passwd and database must be provided,
Following calls can skip this parameters
'''
@@ -172,8 +164,16 @@
except mdb.Error as e:
raise db_base_Exception("Cannot connect to DataBase '{}' at '{}@{}' Error {}: {}".format(
self.database, self.user, self.host, e.args[0], e.args[1]),
- http_code = HTTP_Unauthorized )
-
+ http_code = httperrors.Unauthorized )
+
+ def escape(self, value):
+ return self.con.escape(value)
+
+
+ def escape_string(self, value):
+ return self.con.escape_string(value)
+
+
def get_db_version(self):
''' Obtain the database schema version.
Return: (negative, text) if error or version 0.0 where schema_version table is missing
@@ -212,10 +212,10 @@
if e[0][-5:] == "'con'":
self.logger.warn("while disconnecting from DB: Error %d: %s",e.args[0], e.args[1])
return
- else:
+ else:
raise
- def _format_error(self, e, tries=1, command=None, extra=None):
+ def _format_error(self, e, tries=1, command=None, extra=None, table=None):
'''Creates a text error base on the produced exception
Params:
e: mdb exception
@@ -227,7 +227,8 @@
HTTP error in negative, formatted error text
'''
if isinstance(e,AttributeError ):
- raise db_base_Exception("DB Exception " + str(e), HTTP_Internal_Server_Error)
+ self.logger.debug(str(e), exc_info=True)
+ raise db_base_Exception("DB Exception " + str(e), httperrors.Internal_Server_Error)
if e.args[0]==2006 or e.args[0]==2013 : #MySQL server has gone away (((or))) Exception 2013: Lost connection to MySQL server during query
if tries>1:
self.logger.warn("DB Exception '%s'. Retry", str(e))
@@ -235,32 +236,45 @@
self.connect()
return
else:
- raise db_base_Exception("Database connection timeout Try Again", HTTP_Request_Timeout)
-
+ raise db_base_Exception("Database connection timeout Try Again", httperrors.Request_Timeout)
+
fk=e.args[1].find("foreign key constraint fails")
if fk>=0:
if command=="update":
- raise db_base_Exception("tenant_id '{}' not found.".format(extra), HTTP_Not_Found)
+ raise db_base_Exception("tenant_id '{}' not found.".format(extra), httperrors.Not_Found)
elif command=="delete":
- raise db_base_Exception("Resource is not free. There are {} that prevent deleting it.".format(extra), HTTP_Conflict)
+ raise db_base_Exception("Resource is not free. There are {} that prevent deleting it.".format(extra), httperrors.Conflict)
de = e.args[1].find("Duplicate entry")
fk = e.args[1].find("for key")
uk = e.args[1].find("Unknown column")
wc = e.args[1].find("in 'where clause'")
fl = e.args[1].find("in 'field list'")
#print de, fk, uk, wc,fl
+ table_info = ' (table `{}`)'.format(table) if table else ''
if de>=0:
if fk>=0: #error 1062
- raise db_base_Exception("Value {} already in use for {}".format(e.args[1][de+15:fk], e.args[1][fk+7:]), HTTP_Conflict)
+ raise db_base_Exception(
+ "Value {} already in use for {}{}".format(
+ e.args[1][de+15:fk], e.args[1][fk+7:], table_info),
+ httperrors.Conflict)
if uk>=0:
if wc>=0:
- raise db_base_Exception("Field {} can not be used for filtering".format(e.args[1][uk+14:wc]), HTTP_Bad_Request)
+ raise db_base_Exception(
+ "Field {} can not be used for filtering{}".format(
+ e.args[1][uk+14:wc], table_info),
+ httperrors.Bad_Request)
if fl>=0:
- raise db_base_Exception("Field {} does not exist".format(e.args[1][uk+14:wc]), HTTP_Bad_Request)
- raise db_base_Exception("Database internal Error {}: {}".format(e.args[0], e.args[1]), HTTP_Internal_Server_Error)
-
+ raise db_base_Exception(
+ "Field {} does not exist{}".format(
+ e.args[1][uk+14:wc], table_info),
+ httperrors.Bad_Request)
+ raise db_base_Exception(
+ "Database internal Error{} {}: {}".format(
+ table_info, e.args[0], e.args[1]),
+ httperrors.Internal_Server_Error)
+
def __str2db_format(self, data):
- '''Convert string data to database format.
+ '''Convert string data to database format.
If data is None it returns the 'Null' text,
otherwise it returns the text surrounded by quotes ensuring internal quotes are escaped.
'''
@@ -270,10 +284,10 @@
return json.dumps(data)
else:
return json.dumps(str(data))
-
+
def __tuple2db_format_set(self, data):
"""Compose the needed text for a SQL SET, parameter 'data' is a pair tuple (A,B),
- and it returns the text 'A="B"', where A is a field of a table and B is the value
+ and it returns the text 'A="B"', where A is a field of a table and B is the value
If B is None it returns the 'A=Null' text, without surrounding Null by quotes
If B is not None it returns the text "A='B'" or 'A="B"' where B is surrounded by quotes,
and it ensures internal quotes of B are escaped.
@@ -287,10 +301,10 @@
elif isinstance(data[1], dict):
if "INCREMENT" in data[1]:
return "{A}={A}{N:+d}".format(A=data[0], N=data[1]["INCREMENT"])
- raise db_base_Exception("Format error for UPDATE field")
+ raise db_base_Exception("Format error for UPDATE field: {!r}".format(data[0]))
else:
return str(data[0]) + '=' + json.dumps(str(data[1]))
-
+
def __create_where(self, data, use_or=None):
"""
Compose the needed text for a SQL WHERE, parameter 'data' can be a dict or a list of dict. By default lists are
@@ -347,9 +361,9 @@
'''remove single quotes ' of any string content of data dictionary'''
for k,v in data.items():
if type(v) == str:
- if "'" in v:
+ if "'" in v:
data[k] = data[k].replace("'","_")
-
+
def _update_rows(self, table, UPDATE, WHERE, modified_time=0):
""" Update one or several rows of a table.
:param UPDATE: dictionary with the changes. dict keys are database columns that will be set with the dict values
@@ -369,7 +383,7 @@
values += ",modified_at={:f}".format(modified_time)
cmd= "UPDATE " + table + " SET " + values + " WHERE " + self.__create_where(WHERE)
self.logger.debug(cmd)
- self.cur.execute(cmd)
+ self.cur.execute(cmd)
return self.cur.rowcount
def _new_uuid(self, root_uuid=None, used_table=None, created_time=0):
@@ -398,7 +412,7 @@
def _new_row_internal(self, table, INSERT, add_uuid=False, root_uuid=None, created_time=0, confidential_data=False):
''' Add one row into a table. It DOES NOT begin or end the transaction, so self.con.cursor must be created
- Attribute
+ Attribute
INSERT: dictionary with the key:value to insert
table: table where to insert
add_uuid: if True, it will create an uuid key entry at INSERT if not provided
@@ -411,7 +425,7 @@
#create uuid if not provided
if 'uuid' not in INSERT:
uuid = INSERT['uuid'] = str(myUuid.uuid1()) # create_uuid
- else:
+ else:
uuid = str(INSERT['uuid'])
else:
uuid=None
@@ -429,7 +443,7 @@
self.cur.execute(cmd)
#insertion
cmd= "INSERT INTO " + table +" SET " + \
- ",".join(map(self.__tuple2db_format_set, INSERT.iteritems() ))
+ ",".join(map(self.__tuple2db_format_set, INSERT.iteritems() ))
if created_time:
cmd += ",created_at=%f" % created_time
if confidential_data:
@@ -448,16 +462,16 @@
self.cur.execute(cmd)
rows = self.cur.fetchall()
return rows
-
+
def new_row(self, table, INSERT, add_uuid=False, created_time=0, confidential_data=False):
''' Add one row into a table.
- Attribute
+ Attribute
INSERT: dictionary with the key: value to insert
table: table where to insert
tenant_id: only useful for logs. If provided, logs will use this tenant_id
add_uuid: if True, it will create an uuid key entry at INSERT if not provided
It checks presence of uuid and add one automatically otherwise
- Return: (result, uuid) where result can be 0 if error, or 1 if ok
+ Return: uuid
'''
if table in self.tables_with_created_field and created_time==0:
created_time=time.time()
@@ -467,9 +481,9 @@
with self.con:
self.cur = self.con.cursor()
return self._new_row_internal(table, INSERT, add_uuid, None, created_time, confidential_data)
-
+
except (mdb.Error, AttributeError) as e:
- self._format_error(e, tries)
+ self._format_error(e, tries, table=table)
tries -= 1
def update_rows(self, table, UPDATE, WHERE, modified_time=0):
@@ -493,10 +507,10 @@
try:
with self.con:
self.cur = self.con.cursor()
- return self._update_rows(table, UPDATE, WHERE)
-
+ return self._update_rows(
+ table, UPDATE, WHERE, modified_time)
except (mdb.Error, AttributeError) as e:
- self._format_error(e, tries)
+ self._format_error(e, tries, table=table)
tries -= 1
def _delete_row_by_id_internal(self, table, uuid):
@@ -519,7 +533,8 @@
self.cur = self.con.cursor()
return self._delete_row_by_id_internal(table, uuid)
except (mdb.Error, AttributeError) as e:
- self._format_error(e, tries, "delete", "dependencies")
+ self._format_error(
+ e, tries, "delete", "dependencies", table=table)
tries -= 1
def delete_row(self, **sql_dict):
@@ -567,9 +582,9 @@
rows = self.cur.fetchall()
return rows
except (mdb.Error, AttributeError) as e:
- self._format_error(e, tries)
+ self._format_error(e, tries, table=table)
tries -= 1
-
+
def get_rows(self, **sql_dict):
""" Obtain rows from a table.
:param SELECT: list or tuple of fields to retrieve) (by default all)
@@ -581,7 +596,7 @@
keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "="
The special keys "OR", "AND" with a dict value is used to create a nested WHERE
If a list, each item will be a dictionary that will be concatenated with OR
- :param LIMIT: limit the number of obtianied entries (Optional)
+ :param LIMIT: limit the number of obtained entries (Optional)
:param ORDER_BY: list or tuple of fields to order, add ' DESC' to each item if inverse order is required
:return: a list with dictionaries at each row, raises exception upon error
"""
@@ -628,10 +643,10 @@
Attribute:
table: string of table name
uuid_name: name or uuid. If not uuid format is found, it is considered a name
- allow_severeral: if False return ERROR if more than one row are founded
- error_item_text: in case of error it identifies the 'item' name for a proper output text
+ allow_severeral: if False return ERROR if more than one row are founded
+ error_item_text: in case of error it identifies the 'item' name for a proper output text
'WHERE_OR': dict of key:values, translated to key=value OR ... (Optional)
- 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional
+ 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional
Return: if allow_several==False, a dictionary with this row, or error if no item is found or more than one is found
if allow_several==True, a list of dictionaries with the row or rows, error if no item is found
'''
@@ -656,16 +671,16 @@
self.cur.execute(cmd)
number = self.cur.rowcount
if number == 0:
- raise db_base_Exception("No {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=HTTP_Not_Found)
+ raise db_base_Exception("No {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Not_Found)
elif number > 1 and not allow_serveral:
- raise db_base_Exception("More than one {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=HTTP_Conflict)
+ raise db_base_Exception("More than one {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Conflict)
if allow_serveral:
rows = self.cur.fetchall()
else:
rows = self.cur.fetchone()
return rows
except (mdb.Error, AttributeError) as e:
- self._format_error(e, tries)
+ self._format_error(e, tries, table=table)
tries -= 1
def get_uuid(self, uuid):
@@ -684,7 +699,7 @@
def get_uuid_from_name(self, table, name):
'''Searchs in table the name and returns the uuid
- '''
+ '''
tries = 2
while tries:
try:
@@ -699,6 +714,6 @@
return self.cur.rowcount, "More than one VNF with name %s found in table %s" %(name, table)
return self.cur.rowcount, rows[0]["uuid"]
except (mdb.Error, AttributeError) as e:
- self._format_error(e, tries)
+ self._format_error(e, tries, table=table)
tries -= 1
diff --git a/osm_ro/http_tools/__init__.py b/osm_ro/http_tools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/osm_ro/http_tools/__init__.py
diff --git a/osm_ro/http_tools/errors.py b/osm_ro/http_tools/errors.py
new file mode 100644
index 0000000..552e85b
--- /dev/null
+++ b/osm_ro/http_tools/errors.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+import logging
+from functools import wraps
+
+import bottle
+import yaml
+
+Bad_Request = 400
+Unauthorized = 401
+Not_Found = 404
+Forbidden = 403
+Method_Not_Allowed = 405
+Not_Acceptable = 406
+Request_Timeout = 408
+Conflict = 409
+Service_Unavailable = 503
+Internal_Server_Error = 500
+
+
+class HttpMappedError(Exception):
+ """Base class for a new hierarchy that translate HTTP error codes
+ to python exceptions
+
+ This class accepts an extra argument ``http_code`` (integer
+ representing HTTP error codes).
+ """
+
+ def __init__(self, message, http_code=Internal_Server_Error):
+ Exception.__init__(self, message)
+ self.http_code = http_code
+
+
+class ErrorHandler(object):
+ """Defines a default strategy for handling HttpMappedError.
+
+ This class implements a wrapper (can also be used as decorator), that
+ watches out for different exceptions and log them accordingly.
+
+ Arguments:
+ logger(logging.Logger): logger object to be used to report errors
+ """
+ def __init__(self, logger=None):
+ self.logger = logger or logging.getLogger('openmano.http')
+
+ def __call__(self, function):
+ @wraps(function)
+ def _wraped(*args, **kwargs):
+ try:
+ return function(*args, **kwargs)
+ except bottle.HTTPError:
+ raise
+ except HttpMappedError as ex:
+ self.logger.error(
+ "%s error %s",
+ function.__name__, ex.http_code, exc_info=True)
+ bottle.abort(ex.http_code, str(ex))
+ except yaml.YAMLError as ex:
+ self.logger.error(
+ "YAML error while trying to serialize/unserialize fields",
+ exc_info=True)
+ bottle.abort(Bad_Request, type(ex).__name__ + ": " + str(ex))
+ except Exception as ex:
+ self.logger.error("Unexpected exception: ", exc_info=True)
+ bottle.abort(Internal_Server_Error,
+ type(ex).__name__ + ": " + str(ex))
+
+ return _wraped
diff --git a/osm_ro/http_tools/handler.py b/osm_ro/http_tools/handler.py
new file mode 100644
index 0000000..49249a8
--- /dev/null
+++ b/osm_ro/http_tools/handler.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+from types import MethodType
+
+from bottle import Bottle
+
+
+class route(object):
+ """Decorator that stores route information, so creating the routes can be
+ postponed.
+
+ This allows methods (OOP) with bottle.
+
+ Arguments:
+ method: HTTP verb (e.g. ``'get'``, ``'post'``, ``'put'``, ...)
+ path: URL path that will be handled by the callback
+ """
+ def __init__(self, method, path, **kwargs):
+ kwargs['method'] = method.upper()
+ self.route_info = (path, kwargs)
+
+ def __call__(self, function):
+ function.route_info = self.route_info
+ return function
+
+
+class BaseHandler(object):
+ """Base class that allows isolated webapp implementation using Bottle,
+ when used in conjunction with the ``route`` decorator.
+
+ In this context, a ``Handler`` is meant to be a collection of Bottle
+ routes/callbacks related to a specific topic.
+
+ A ``Handler`` instance can produce a WSGI app that can be mounted or merged
+ inside another more general bottle app.
+
+ Example:
+
+ from http_tools.handler import Handler, route
+ from http_tools.errors import ErrorHandler
+
+ class MyHandler(Handler):
+ plugins = [ErrorHandler()]
+ url_base = '/my/url/base'
+
+ @route('GET', '/some/path/<var>')
+ def get_var(self, var):
+ return var
+
+ app = MyHandler.wsgi_app
+ # ^ Webapp with a `GET /my/url/base/some/path/<var>` route
+ """
+ _wsgi_app = None
+
+ url_base = ''
+ """String representing a path fragment to be prepended to the routes"""
+
+ plugins = []
+ """Bottle plugins to be installed when creating the WSGI app"""
+
+ @property
+ def wsgi_app(self):
+ """Create a WSGI app based on the implemented callbacks"""
+
+ if self._wsgi_app:
+ # Return if cached
+ return self._wsgi_app
+
+ app = Bottle()
+
+ members = (getattr(self, m) for m in dir(self) if m != 'wsgi_app')
+ callbacks = (m for m in members
+ if isinstance(m, MethodType) and hasattr(m, 'route_info'))
+
+ for callback in callbacks:
+ path, kwargs = callback.route_info
+ kwargs.update(callback=callback, apply=self.plugins)
+ app.route(self.url_base + path, **kwargs)
+
+ self._wsgi_app = app
+
+ return app
diff --git a/osm_ro/http_tools/request_processing.py b/osm_ro/http_tools/request_processing.py
new file mode 100644
index 0000000..0b8a6ca
--- /dev/null
+++ b/osm_ro/http_tools/request_processing.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+
+#
+# Util functions previously in `httpserver`
+#
+
+__author__ = "Alfonso Tierno, Gerardo Garcia"
+
+import json
+import logging
+
+import bottle
+import yaml
+from jsonschema import exceptions as js_e
+from jsonschema import validate as js_v
+
+from . import errors as httperrors
+
+logger = logging.getLogger('openmano.http')
+
+
+def remove_clear_passwd(data):
+ """
+ Removes clear passwords from the data received
+ :param data: data with clear password
+ :return: data without the password information
+ """
+
+ passw = ['password: ', 'passwd: ']
+
+ for pattern in passw:
+ init = data.find(pattern)
+ while init != -1:
+ end = data.find('\n', init)
+ data = data[:init] + '{}******'.format(pattern) + data[end:]
+ init += 1
+ init = data.find(pattern, init)
+ return data
+
+
+def change_keys_http2db(data, http_db, reverse=False):
+ '''Change keys of dictionary data acording to the key_dict values
+ This allow change from http interface names to database names.
+ When reverse is True, the change is otherwise
+ Attributes:
+ data: can be a dictionary or a list
+ http_db: is a dictionary with hhtp names as keys and database names as value
+ reverse: by default change is done from http api to database.
+ If True change is done otherwise.
+ Return: None, but data is modified'''
+ if type(data) is tuple or type(data) is list:
+ for d in data:
+ change_keys_http2db(d, http_db, reverse)
+ elif type(data) is dict or type(data) is bottle.FormsDict:
+ if reverse:
+ for k,v in http_db.items():
+ if v in data: data[k]=data.pop(v)
+ else:
+ for k,v in http_db.items():
+ if k in data: data[v]=data.pop(k)
+
+
+def format_out(data):
+ '''Return string of dictionary data according to requested json, yaml, xml.
+ By default json
+ '''
+ logger.debug("OUT: " + yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False, encoding='utf-8', allow_unicode=True) )
+ accept = bottle.request.headers.get('Accept')
+ if accept and 'application/yaml' in accept:
+ bottle.response.content_type='application/yaml'
+ return yaml.safe_dump(
+ data, explicit_start=True, indent=4, default_flow_style=False,
+ tags=False, encoding='utf-8', allow_unicode=True) #, canonical=True, default_style='"'
+ else: #by default json
+ bottle.response.content_type='application/json'
+ #return data #json no style
+ return json.dumps(data, indent=4) + "\n"
+
+
+def format_in(default_schema, version_fields=None, version_dict_schema=None, confidential_data=False):
+ """
+ Parse the content of HTTP request against a json_schema
+
+ :param default_schema: The schema to be parsed by default
+ if no version field is found in the client data.
+ In None no validation is done
+ :param version_fields: If provided it contains a tuple or list with the
+ fields to iterate across the client data to obtain the version
+ :param version_dict_schema: It contains a dictionary with the version as key,
+ and json schema to apply as value.
+ It can contain a None as key, and this is apply
+ if the client data version does not match any key
+ :return: user_data, used_schema: if the data is successfully decoded and
+ matches the schema.
+
+ Launch a bottle abort if fails
+ """
+ #print "HEADERS :" + str(bottle.request.headers.items())
+ try:
+ error_text = "Invalid header format "
+ format_type = bottle.request.headers.get('Content-Type', 'application/json')
+ if 'application/json' in format_type:
+ error_text = "Invalid json format "
+ #Use the json decoder instead of bottle decoder because it informs about the location of error formats with a ValueError exception
+ client_data = json.load(bottle.request.body)
+ #client_data = bottle.request.json()
+ elif 'application/yaml' in format_type:
+ error_text = "Invalid yaml format "
+ client_data = yaml.load(bottle.request.body)
+ elif 'application/xml' in format_type:
+ bottle.abort(501, "Content-Type: application/xml not supported yet.")
+ else:
+ logger.warning('Content-Type ' + str(format_type) + ' not supported.')
+ bottle.abort(httperrors.Not_Acceptable, 'Content-Type ' + str(format_type) + ' not supported.')
+ return
+ # if client_data == None:
+ # bottle.abort(httperrors.Bad_Request, "Content error, empty")
+ # return
+ if confidential_data:
+ logger.debug('IN: %s', remove_clear_passwd (yaml.safe_dump(client_data, explicit_start=True, indent=4, default_flow_style=False,
+ tags=False, encoding='utf-8', allow_unicode=True)))
+ else:
+ logger.debug('IN: %s', yaml.safe_dump(client_data, explicit_start=True, indent=4, default_flow_style=False,
+ tags=False, encoding='utf-8', allow_unicode=True) )
+ # look for the client provider version
+ error_text = "Invalid content "
+ if not default_schema and not version_fields:
+ return client_data, None
+ client_version = None
+ used_schema = None
+ if version_fields != None:
+ client_version = client_data
+ for field in version_fields:
+ if field in client_version:
+ client_version = client_version[field]
+ else:
+ client_version=None
+ break
+ if client_version == None:
+ used_schema = default_schema
+ elif version_dict_schema != None:
+ if client_version in version_dict_schema:
+ used_schema = version_dict_schema[client_version]
+ elif None in version_dict_schema:
+ used_schema = version_dict_schema[None]
+ if used_schema==None:
+ bottle.abort(httperrors.Bad_Request, "Invalid schema version or missing version field")
+
+ js_v(client_data, used_schema)
+ return client_data, used_schema
+ except (TypeError, ValueError, yaml.YAMLError) as exc:
+ error_text += str(exc)
+ logger.error(error_text, exc_info=True)
+ bottle.abort(httperrors.Bad_Request, error_text)
+ except js_e.ValidationError as exc:
+ logger.error(
+ "validate_in error, jsonschema exception", exc_info=True)
+ error_pos = ""
+ if len(exc.path)>0: error_pos=" at " + ":".join(map(json.dumps, exc.path))
+ bottle.abort(httperrors.Bad_Request, error_text + exc.message + error_pos)
+ #except:
+ # bottle.abort(httperrors.Bad_Request, "Content error: Failed to parse Content-Type", error_pos)
+ # raise
+
+def filter_query_string(qs, http2db, allowed):
+ '''Process query string (qs) checking that contains only valid tokens for avoiding SQL injection
+ Attributes:
+ 'qs': bottle.FormsDict variable to be processed. None or empty is considered valid
+ 'http2db': dictionary with change from http API naming (dictionary key) to database naming(dictionary value)
+ 'allowed': list of allowed string tokens (API http naming). All the keys of 'qs' must be one of 'allowed'
+ Return: A tuple with the (select,where,limit) to be use in a database query. All of then transformed to the database naming
+ select: list of items to retrieve, filtered by query string 'field=token'. If no 'field' is present, allowed list is returned
+ where: dictionary with key, value, taken from the query string token=value. Empty if nothing is provided
+ limit: limit dictated by user with the query string 'limit'. 100 by default
+ abort if not permited, using bottel.abort
+ '''
+ where={}
+ limit=100
+ select=[]
+ #if type(qs) is not bottle.FormsDict:
+ # bottle.abort(httperrors.Internal_Server_Error, '!!!!!!!!!!!!!!invalid query string not a dictionary')
+ # #bottle.abort(httperrors.Internal_Server_Error, "call programmer")
+ for k in qs:
+ if k=='field':
+ select += qs.getall(k)
+ for v in select:
+ if v not in allowed:
+ bottle.abort(httperrors.Bad_Request, "Invalid query string at 'field="+v+"'")
+ elif k=='limit':
+ try:
+ limit=int(qs[k])
+ except:
+ bottle.abort(httperrors.Bad_Request, "Invalid query string at 'limit="+qs[k]+"'")
+ else:
+ if k not in allowed:
+ bottle.abort(httperrors.Bad_Request, "Invalid query string at '"+k+"="+qs[k]+"'")
+ if qs[k]!="null": where[k]=qs[k]
+ else: where[k]=None
+ if len(select)==0: select += allowed
+ #change from http api to database naming
+ for i in range(0,len(select)):
+ k=select[i]
+ if http2db and k in http2db:
+ select[i] = http2db[k]
+ if http2db:
+ change_keys_http2db(where, http2db)
+ #print "filter_query_string", select,where,limit
+
+ return select,where,limit
diff --git a/osm_ro/http_tools/tests/__init__.py b/osm_ro/http_tools/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/osm_ro/http_tools/tests/__init__.py
diff --git a/osm_ro/http_tools/tests/test_errors.py b/osm_ro/http_tools/tests/test_errors.py
new file mode 100644
index 0000000..a968e76
--- /dev/null
+++ b/osm_ro/http_tools/tests/test_errors.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+import bottle
+
+from .. import errors as httperrors
+from ...tests.helpers import TestCaseWithLogging
+
+
+class TestHttpErrors(TestCaseWithLogging):
+ def test_http_error_base(self):
+ # When an error code is passed as argument
+ ex = httperrors.HttpMappedError(http_code=1226324)
+ # then it should be set in the exception object
+ self.assertEqual(ex.http_code, 1226324)
+ # When an error code is not passed as argument
+ ex = httperrors.HttpMappedError()
+ # then the default error code (internal server error) should be used
+ self.assertEqual(ex.http_code, httperrors.Internal_Server_Error)
+
+ def test_error_handler_should_log_unexpected_errors(self):
+ # Given a error handler wraps a function
+ error_handler = httperrors.ErrorHandler(self.logger)
+
+ # and the function raises an unexpected error
+ @error_handler
+ def _throw():
+ raise AttributeError('some error')
+
+ # when the function is called
+ with self.assertRaises(bottle.HTTPError):
+ _throw()
+ logs = self.caplog.getvalue()
+ # then the exception should be contained by bottle
+ # and a proper message should be logged
+ assert "Unexpected exception:" in logs
+
+ def test_error_handler_should_log_http_based_errors(self):
+ # Given a error handler wraps a function
+ error_handler = httperrors.ErrorHandler(self.logger)
+
+ # and the function raises an error that is considered by the
+ # application
+ @error_handler
+ def _throw():
+ raise httperrors.HttpMappedError(http_code=404)
+
+ # when the function is called
+ with self.assertRaises(bottle.HTTPError):
+ _throw()
+ logs = self.caplog.getvalue()
+ # then the exception should be contained by bottle
+ # and a proper message should be logged
+ assert "_throw error 404" in logs
+
+ def test_error_handler_should_ignore_bottle_errors(self):
+ # Given a error handler wraps a function
+ error_handler = httperrors.ErrorHandler(self.logger)
+
+ # and the function raises an error that is considered by the
+ # application
+ exception = bottle.HTTPError()
+
+ @error_handler
+ def _throw():
+ raise exception
+
+ # when the function is called
+ with self.assertRaises(bottle.HTTPError) as context:
+ _throw()
+ # then the exception should bypass the error handler
+ self.assertEqual(context.exception, exception)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/osm_ro/http_tools/tests/test_handler.py b/osm_ro/http_tools/tests/test_handler.py
new file mode 100644
index 0000000..af32545
--- /dev/null
+++ b/osm_ro/http_tools/tests/test_handler.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+from mock import MagicMock, patch
+from webtest import TestApp
+
+from .. import handler
+from ..handler import BaseHandler, route
+
+
+class TestIntegration(unittest.TestCase):
+ def test_wsgi_app(self):
+ # Given a Handler class that implements a route
+ some_plugin = MagicMock()
+
+ class MyHandler(BaseHandler):
+ url_base = '/42'
+ plugins = [some_plugin]
+
+ @route('get', '/some/path')
+ def callback(self):
+ return 'some content'
+
+ route_mock = MagicMock()
+ with patch(handler.__name__+'.Bottle.route', route_mock):
+ # When we try to access wsgi_app for the first time
+ my_handler = MyHandler()
+ assert my_handler.wsgi_app
+ # then bottle.route should be called with the right arguments
+ route_mock.assert_called_once_with('/42/some/path', method='GET',
+ callback=my_handler.callback,
+ apply=[some_plugin])
+
+ # When we try to access wsgi_app for the second time
+ assert my_handler.wsgi_app
+ # then the result should be cached
+ # and bottle.route should not be called again
+ self.assertEqual(route_mock.call_count, 1)
+
+ def test_route_created(self):
+ # Given a Handler class, as in the example documentation
+ class MyHandler(BaseHandler):
+ def __init__(self):
+ self.value = 42
+
+ @route('GET', '/some/path/<param>')
+ def callback(self, param):
+ return '{} + {}'.format(self.value, param)
+
+ # when this class is used to generate a webapp
+ app = TestApp(MyHandler().wsgi_app)
+
+ # then the defined URLs should be available
+ response = app.get('/some/path/0')
+ self.assertEqual(response.status_code, 200)
+ # and the callbacks should have access to ``self``
+ response.mustcontain('42 + 0')
+
+ def test_url_base(self):
+ # Given a Handler class that allows url_base customization
+ class MyHandler(BaseHandler):
+ def __init__(self, url_base):
+ self.url_base = url_base
+
+ @route('GET', '/some/path/<param>')
+ def callback(self, param):
+ return param
+
+ # when this class is used to generate a webapp
+ app = TestApp(MyHandler('/prefix').wsgi_app)
+
+ # then the prefixed URLs should be available
+ response = app.get('/prefix/some/path/content')
+ self.assertEqual(response.status_code, 200)
+ response.mustcontain('content')
+
+ def test_starting_param(self):
+ # Given a Handler class with a route beginning with a param
+ class MyHandler(BaseHandler):
+ @route('GET', '/<param>/some/path')
+ def callback(self, param):
+ return '**{}**'.format(param)
+
+ # is used to generate a webapp
+ app = TestApp(MyHandler().wsgi_app)
+
+ # when the defined URLs is accessed
+ response = app.get('/42/some/path')
+ # Then no error should happen
+ self.assertEqual(response.status_code, 200)
+ response.mustcontain('**42**')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/osm_ro/http_tools/tox.ini b/osm_ro/http_tools/tox.ini
new file mode 100644
index 0000000..43055c2
--- /dev/null
+++ b/osm_ro/http_tools/tox.ini
@@ -0,0 +1,49 @@
+# This tox file allows the devs to run unit tests only for this subpackage.
+# In order to do so, cd into the directory and run `tox`
+
+[tox]
+minversion = 1.8
+envlist = py27,py36,flake8,radon
+skipsdist = True
+
+[testenv]
+changedir = {toxinidir}
+commands =
+ nosetests -d --with-coverage --cover-package=. {posargs:tests}
+deps =
+ WebTest
+ bottle
+ coverage
+ mock
+ nose
+ six
+ PyYaml
+
+[testenv:flake8]
+changedir = {toxinidir}
+deps = flake8
+commands = flake8 {posargs:.}
+
+[testenv:radon]
+changedir = {toxinidir}
+deps = radon
+commands =
+ radon cc --show-complexity --total-average {posargs:.}
+ radon mi -s {posargs:.}
+
+[coverage:run]
+branch = True
+source = {toxinidir}
+omit =
+ tests
+ tests/*
+ */test_*
+ .tox/*
+
+[coverage:report]
+show_missing = True
+
+[flake8]
+exclude =
+ request_processing.py
+ .tox
diff --git a/osm_ro/httpserver.py b/osm_ro/httpserver.py
index 374676e..613fb08 100644
--- a/osm_ro/httpserver.py
+++ b/osm_ro/httpserver.py
@@ -24,19 +24,16 @@
'''
HTTP server implementing the openmano API. It will answer to POST, PUT, GET methods in the appropriate URLs
and will use the nfvo.py module to run the appropriate method.
-Every YAML/JSON file is checked against a schema in openmano_schemas.py module.
+Every YAML/JSON file is checked against a schema in openmano_schemas.py module.
'''
__author__="Alfonso Tierno, Gerardo Garcia"
__date__ ="$17-sep-2014 09:07:15$"
import bottle
import yaml
-import json
import threading
-import time
import logging
-from jsonschema import validate as js_v, exceptions as js_e
from openmano_schemas import vnfd_schema_v01, vnfd_schema_v02, \
nsd_schema_v01, nsd_schema_v02, nsd_schema_v03, scenario_edit_schema, \
scenario_action_schema, instance_scenario_action_schema, instance_scenario_create_schema_v01, \
@@ -45,6 +42,14 @@
object_schema, netmap_new_schema, netmap_edit_schema, sdn_controller_schema, sdn_controller_edit_schema, \
sdn_port_mapping_schema, sdn_external_port_schema
+from .http_tools import errors as httperrors
+from .http_tools.request_processing import (
+ format_out,
+ format_in,
+ filter_query_string
+)
+from .wim.http_handler import WimHandler
+
import nfvo
import utils
from db_base import db_base_Exception
@@ -56,42 +61,6 @@
url_base="/openmano"
logger = None
-HTTP_Bad_Request = 400
-HTTP_Unauthorized = 401
-HTTP_Not_Found = 404
-HTTP_Forbidden = 403
-HTTP_Method_Not_Allowed = 405
-HTTP_Not_Acceptable = 406
-HTTP_Service_Unavailable = 503
-HTTP_Internal_Server_Error= 500
-
-def delete_nulls(var):
- if type(var) is dict:
- for k in var.keys():
- if var[k] is None: del var[k]
- elif type(var[k]) is dict or type(var[k]) is list or type(var[k]) is tuple:
- if delete_nulls(var[k]): del var[k]
- if len(var) == 0: return True
- elif type(var) is list or type(var) is tuple:
- for k in var:
- if type(k) is dict: delete_nulls(k)
- if len(var) == 0: return True
- return False
-
-def convert_datetime2str(var):
- '''Converts a datetime variable to a string with the format '%Y-%m-%dT%H:%i:%s'
- It enters recursively in the dict var finding this kind of variables
- '''
- if type(var) is dict:
- for k,v in var.items():
- if type(v) is float and k in ("created_at", "modified_at"):
- var[k] = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(v) )
- elif type(v) is dict or type(v) is list or type(v) is tuple:
- convert_datetime2str(v)
- if len(var) == 0: return True
- elif type(var) is list or type(var) is tuple:
- for v in var:
- convert_datetime2str(v)
def log_to_logger(fn):
'''
@@ -102,15 +71,16 @@
def _log_to_logger(*args, **kwargs):
actual_response = fn(*args, **kwargs)
# modify this to log exactly what you need:
- logger.info('FROM %s %s %s %s' % (bottle.request.remote_addr,
+ logger.info('FROM %s %s %s %s', bottle.request.remote_addr,
bottle.request.method,
bottle.request.url,
- bottle.response.status))
+ bottle.response.status)
return actual_response
return _log_to_logger
class httpserver(threading.Thread):
- def __init__(self, db, admin=False, host='localhost', port=9090):
+ def __init__(self, db, admin=False, host='localhost', port=9090,
+ wim_persistence=None, wim_engine=None):
#global url_base
global mydb
global logger
@@ -127,186 +97,37 @@
#self.url_preffix = 'http://' + host + ':' + str(port) + url_base
mydb = db
#self.first_usable_connection_index = 10
- #self.next_connection_index = self.first_usable_connection_index #The next connection index to be used
+ #self.next_connection_index = self.first_usable_connection_index #The next connection index to be used
#Ensure that when the main program exits the thread will also exit
+
+ self.handlers = [
+ WimHandler(db, wim_persistence, wim_engine, url_base)
+ ]
+
self.daemon = True
self.setDaemon(True)
-
- def run(self):
+
+ def run(self, debug=False, quiet=True):
bottle.install(log_to_logger)
- bottle.run(host=self.host, port=self.port, debug=False, quiet=True)
-
+ default_app = bottle.app()
+
+ for handler in self.handlers:
+ default_app.merge(handler.wsgi_app)
+
+ bottle.run(host=self.host, port=self.port, debug=debug, quiet=quiet)
+
+
def run_bottle(db, host_='localhost', port_=9090):
- '''used for launching in main thread, so that it can be debugged'''
- global mydb
- mydb = db
- bottle.run(host=host_, port=port_, debug=True) #quiet=True
-
+ '''Used for launching in main thread, so that it can be debugged'''
+ server = httpserver(db, host=host_, port=port_)
+ server.run(debug=True) # quiet=True
+
@bottle.route(url_base + '/', method='GET')
def http_get():
- #print
+ #print
return 'works' #TODO: to be completed
-#
-# Util functions
-#
-
-def change_keys_http2db(data, http_db, reverse=False):
- '''Change keys of dictionary data acording to the key_dict values
- This allow change from http interface names to database names.
- When reverse is True, the change is otherwise
- Attributes:
- data: can be a dictionary or a list
- http_db: is a dictionary with hhtp names as keys and database names as value
- reverse: by default change is done from http api to database. If True change is done otherwise
- Return: None, but data is modified'''
- if type(data) is tuple or type(data) is list:
- for d in data:
- change_keys_http2db(d, http_db, reverse)
- elif type(data) is dict or type(data) is bottle.FormsDict:
- if reverse:
- for k,v in http_db.items():
- if v in data: data[k]=data.pop(v)
- else:
- for k,v in http_db.items():
- if k in data: data[v]=data.pop(k)
-
-def format_out(data):
- '''return string of dictionary data according to requested json, yaml, xml. By default json'''
- logger.debug("OUT: " + yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False, encoding='utf-8', allow_unicode=True) )
- if 'application/yaml' in bottle.request.headers.get('Accept'):
- bottle.response.content_type='application/yaml'
- return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False, encoding='utf-8', allow_unicode=True) #, canonical=True, default_style='"'
- else: #by default json
- bottle.response.content_type='application/json'
- #return data #json no style
- return json.dumps(data, indent=4) + "\n"
-
-def format_in(default_schema, version_fields=None, version_dict_schema=None, confidential_data=False):
- """
- Parse the content of HTTP request against a json_schema
- :param default_schema: The schema to be parsed by default if no version field is found in the client data. In None
- no validation is done
- :param version_fields: If provided it contains a tuple or list with the fields to iterate across the client data to
- obtain the version
- :param version_dict_schema: It contains a dictionary with the version as key, and json schema to apply as value.
- It can contain a None as key, and this is apply if the client data version does not match any key
- :return: user_data, used_schema: if the data is successfully decoded and matches the schema.
- Launch a bottle abort if fails
- """
- #print "HEADERS :" + str(bottle.request.headers.items())
- try:
- error_text = "Invalid header format "
- format_type = bottle.request.headers.get('Content-Type', 'application/json')
- if 'application/json' in format_type:
- error_text = "Invalid json format "
- #Use the json decoder instead of bottle decoder because it informs about the location of error formats with a ValueError exception
- client_data = json.load(bottle.request.body)
- #client_data = bottle.request.json()
- elif 'application/yaml' in format_type:
- error_text = "Invalid yaml format "
- client_data = yaml.load(bottle.request.body)
- elif 'application/xml' in format_type:
- bottle.abort(501, "Content-Type: application/xml not supported yet.")
- else:
- logger.warning('Content-Type ' + str(format_type) + ' not supported.')
- bottle.abort(HTTP_Not_Acceptable, 'Content-Type ' + str(format_type) + ' not supported.')
- return
- # if client_data == None:
- # bottle.abort(HTTP_Bad_Request, "Content error, empty")
- # return
- if confidential_data:
- logger.debug('IN: %s', remove_clear_passwd (yaml.safe_dump(client_data, explicit_start=True, indent=4, default_flow_style=False,
- tags=False, encoding='utf-8', allow_unicode=True)))
- else:
- logger.debug('IN: %s', yaml.safe_dump(client_data, explicit_start=True, indent=4, default_flow_style=False,
- tags=False, encoding='utf-8', allow_unicode=True) )
- # look for the client provider version
- error_text = "Invalid content "
- if not default_schema and not version_fields:
- return client_data, None
- client_version = None
- used_schema = None
- if version_fields != None:
- client_version = client_data
- for field in version_fields:
- if field in client_version:
- client_version = client_version[field]
- else:
- client_version=None
- break
- if client_version == None:
- used_schema = default_schema
- elif version_dict_schema != None:
- if client_version in version_dict_schema:
- used_schema = version_dict_schema[client_version]
- elif None in version_dict_schema:
- used_schema = version_dict_schema[None]
- if used_schema==None:
- bottle.abort(HTTP_Bad_Request, "Invalid schema version or missing version field")
-
- js_v(client_data, used_schema)
- return client_data, used_schema
- except (TypeError, ValueError, yaml.YAMLError) as exc:
- error_text += str(exc)
- logger.error(error_text)
- bottle.abort(HTTP_Bad_Request, error_text)
- except js_e.ValidationError as exc:
- logger.error("validate_in error, jsonschema exception at '%s' '%s' ", str(exc.path), str(exc.message))
- error_pos = ""
- if len(exc.path)>0: error_pos=" at " + ":".join(map(json.dumps, exc.path))
- bottle.abort(HTTP_Bad_Request, error_text + exc.message + error_pos)
- #except:
- # bottle.abort(HTTP_Bad_Request, "Content error: Failed to parse Content-Type", error_pos)
- # raise
-
-def filter_query_string(qs, http2db, allowed):
- '''Process query string (qs) checking that contains only valid tokens for avoiding SQL injection
- Attributes:
- 'qs': bottle.FormsDict variable to be processed. None or empty is considered valid
- 'http2db': dictionary with change from http API naming (dictionary key) to database naming(dictionary value)
- 'allowed': list of allowed string tokens (API http naming). All the keys of 'qs' must be one of 'allowed'
- Return: A tuple with the (select,where,limit) to be use in a database query. All of then transformed to the database naming
- select: list of items to retrieve, filtered by query string 'field=token'. If no 'field' is present, allowed list is returned
- where: dictionary with key, value, taken from the query string token=value. Empty if nothing is provided
- limit: limit dictated by user with the query string 'limit'. 100 by default
- abort if not permited, using bottel.abort
- '''
- where={}
- limit=100
- select=[]
- #if type(qs) is not bottle.FormsDict:
- # bottle.abort(HTTP_Internal_Server_Error, '!!!!!!!!!!!!!!invalid query string not a dictionary')
- # #bottle.abort(HTTP_Internal_Server_Error, "call programmer")
- for k in qs:
- if k=='field':
- select += qs.getall(k)
- for v in select:
- if v not in allowed:
- bottle.abort(HTTP_Bad_Request, "Invalid query string at 'field="+v+"'")
- elif k=='limit':
- try:
- limit=int(qs[k])
- except:
- bottle.abort(HTTP_Bad_Request, "Invalid query string at 'limit="+qs[k]+"'")
- else:
- if k not in allowed:
- bottle.abort(HTTP_Bad_Request, "Invalid query string at '"+k+"="+qs[k]+"'")
- if qs[k]!="null": where[k]=qs[k]
- else: where[k]=None
- if len(select)==0: select += allowed
- #change from http api to database naming
- for i in range(0,len(select)):
- k=select[i]
- if http2db and k in http2db:
- select[i] = http2db[k]
- if http2db:
- change_keys_http2db(where, http2db)
- #print "filter_query_string", select,where,limit
-
- return select,where,limit
-
@bottle.hook('after_request')
def enable_cors():
'''Don't know yet if really needed. Keep it just in case'''
@@ -327,7 +148,7 @@
try:
tenants = mydb.get_rows(FROM='nfvo_tenants', SELECT=select_,WHERE=where_,LIMIT=limit_)
#change_keys_http2db(content, http2db_tenant, reverse=True)
- convert_datetime2str(tenants)
+ utils.convert_float_timestamp2str(tenants)
data={'tenants' : tenants}
return format_out(data)
except bottle.HTTPError:
@@ -337,7 +158,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/tenants/<tenant_id>', method='GET')
@@ -354,10 +175,10 @@
tenants = mydb.get_rows(FROM=from_, SELECT=select_,WHERE=where_)
#change_keys_http2db(content, http2db_tenant, reverse=True)
if len(tenants) == 0:
- bottle.abort(HTTP_Not_Found, "No tenant found with {}='{}'".format(what, tenant_id))
+ bottle.abort(httperrors.Not_Found, "No tenant found with {}='{}'".format(what, tenant_id))
elif len(tenants) > 1:
- bottle.abort(HTTP_Bad_Request, "More than one tenant found with {}='{}'".format(what, tenant_id))
- convert_datetime2str(tenants[0])
+ bottle.abort(httperrors.Bad_Request, "More than one tenant found with {}='{}'".format(what, tenant_id))
+ utils.convert_float_timestamp2str(tenants[0])
data = {'tenant': tenants[0]}
return format_out(data)
except bottle.HTTPError:
@@ -367,7 +188,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/tenants', method='POST')
@@ -379,7 +200,7 @@
r = utils.remove_extra_items(http_content, tenant_schema)
if r:
logger.debug("Remove received extra items %s", str(r))
- try:
+ try:
data = nfvo.new_tenant(mydb, http_content['tenant'])
return http_get_tenant_id(data)
except bottle.HTTPError:
@@ -389,7 +210,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/tenants/<tenant_id>', method='PUT')
@@ -401,11 +222,11 @@
r = utils.remove_extra_items(http_content, tenant_edit_schema)
if r:
logger.debug("Remove received extra items %s", str(r))
-
+
#obtain data, check that only one exist
- try:
+ try:
tenant = mydb.get_table_by_uuid_name('nfvo_tenants', tenant_id)
- #edit data
+ #edit data
tenant_id = tenant['uuid']
where={'uuid': tenant['uuid']}
mydb.update_rows('nfvo_tenants', http_content['tenant'], where)
@@ -417,7 +238,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/tenants/<tenant_id>', method='DELETE')
@@ -434,7 +255,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters', method='GET')
@@ -458,7 +279,7 @@
datacenters = mydb.get_rows(FROM='datacenters',
SELECT=select_,WHERE=where_,LIMIT=limit_)
#change_keys_http2db(content, http2db_tenant, reverse=True)
- convert_datetime2str(datacenters)
+ utils.convert_float_timestamp2str(datacenters)
data={'datacenters' : datacenters}
return format_out(data)
except bottle.HTTPError:
@@ -468,7 +289,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vim_accounts', method='GET')
@@ -542,11 +363,11 @@
SELECT=select_,
FROM=from_,
WHERE=where_)
-
+
if len(datacenters)==0:
- bottle.abort( HTTP_Not_Found, "No datacenter found for tenant with {} '{}'".format(what, datacenter_id) )
- elif len(datacenters)>1:
- bottle.abort( HTTP_Bad_Request, "More than one datacenter found for tenant with {} '{}'".format(what, datacenter_id) )
+ bottle.abort( httperrors.Not_Found, "No datacenter found for tenant with {} '{}'".format(what, datacenter_id) )
+ elif len(datacenters)>1:
+ bottle.abort( httperrors.Bad_Request, "More than one datacenter found for tenant with {} '{}'".format(what, datacenter_id) )
datacenter = datacenters[0]
if tenant_id != 'any':
#get vim tenant info
@@ -586,7 +407,7 @@
except Exception as e:
logger.error("Exception '%s' while trying to load config information", str(e))
#change_keys_http2db(content, http2db_datacenter, reverse=True)
- convert_datetime2str(datacenter)
+ utils.convert_float_timestamp2str(datacenter)
data={'datacenter' : datacenter}
return format_out(data)
except bottle.HTTPError:
@@ -596,7 +417,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/datacenters', method='POST')
@@ -618,7 +439,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/datacenters/<datacenter_id_name>', method='PUT')
@@ -630,7 +451,7 @@
r = utils.remove_extra_items(http_content, datacenter_edit_schema)
if r:
logger.debug("Remove received extra items %s", str(r))
-
+
try:
datacenter_id = nfvo.edit_datacenter(mydb, datacenter_id_name, http_content['datacenter'])
return http_get_datacenter_id('any', datacenter_id)
@@ -641,7 +462,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/sdn_controllers', method='POST')
def http_post_sdn_controller(tenant_id):
@@ -662,7 +483,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/sdn_controllers/<controller_id>', method='PUT')
def http_put_sdn_controller_update(tenant_id, controller_id):
@@ -687,7 +508,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/sdn_controllers', method='GET')
def http_get_sdn_controller(tenant_id):
@@ -704,7 +525,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/sdn_controllers/<controller_id>', method='GET')
def http_get_sdn_controller_id(tenant_id, controller_id):
@@ -720,7 +541,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/sdn_controllers/<controller_id>', method='DELETE')
def http_delete_sdn_controller_id(tenant_id, controller_id):
@@ -736,7 +557,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/sdn_mapping', method='POST')
def http_post_datacenter_sdn_port_mapping(tenant_id, datacenter_id):
@@ -757,7 +578,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/sdn_mapping', method='GET')
def http_get_datacenter_sdn_port_mapping(tenant_id, datacenter_id):
@@ -774,7 +595,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/sdn_mapping', method='DELETE')
def http_delete_datacenter_sdn_port_mapping(tenant_id, datacenter_id):
@@ -790,7 +611,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/networks', method='GET') #deprecated
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/netmaps', method='GET')
@@ -800,7 +621,7 @@
logger.debug('FROM %s %s %s', bottle.request.remote_addr, bottle.request.method, bottle.request.url)
#obtain data
try:
- datacenter_dict = mydb.get_table_by_uuid_name('datacenters', datacenter_id, "datacenter")
+ datacenter_dict = mydb.get_table_by_uuid_name('datacenters', datacenter_id, "datacenter")
where_= {"datacenter_id":datacenter_dict['uuid']}
if netmap_id:
if utils.check_valid_uuid(netmap_id):
@@ -809,14 +630,14 @@
where_["name"] = netmap_id
netmaps =mydb.get_rows(FROM='datacenter_nets',
SELECT=('name','vim_net_id as vim_id', 'uuid', 'type','multipoint','shared','description', 'created_at'),
- WHERE=where_ )
- convert_datetime2str(netmaps)
+ WHERE=where_ )
+ utils.convert_float_timestamp2str(netmaps)
utils.convert_str2boolean(netmaps, ('shared', 'multipoint') )
if netmap_id and len(netmaps)==1:
data={'netmap' : netmaps[0]}
elif netmap_id and len(netmaps)==0:
- bottle.abort(HTTP_Not_Found, "No netmap found with " + " and ".join(map(lambda x: str(x[0])+": "+str(x[1]), where_.iteritems())) )
- return
+ bottle.abort(httperrors.Not_Found, "No netmap found with " + " and ".join(map(lambda x: str(x[0])+": "+str(x[1]), where_.iteritems())) )
+ return
else:
data={'netmaps' : netmaps}
return format_out(data)
@@ -827,7 +648,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/netmaps', method='DELETE')
@@ -837,7 +658,7 @@
logger.debug('FROM %s %s %s', bottle.request.remote_addr, bottle.request.method, bottle.request.url)
#obtain data
try:
- datacenter_dict = mydb.get_table_by_uuid_name('datacenters', datacenter_id, "datacenter")
+ datacenter_dict = mydb.get_table_by_uuid_name('datacenters', datacenter_id, "datacenter")
where_= {"datacenter_id":datacenter_dict['uuid']}
if netmap_id:
if utils.check_valid_uuid(netmap_id):
@@ -845,9 +666,9 @@
else:
where_["name"] = netmap_id
#change_keys_http2db(content, http2db_tenant, reverse=True)
- deleted = mydb.delete_row(FROM='datacenter_nets', WHERE= where_)
+ deleted = mydb.delete_row(FROM='datacenter_nets', WHERE= where_)
if deleted == 0 and netmap_id:
- bottle.abort(HTTP_Not_Found, "No netmap found with " + " and ".join(map(lambda x: str(x[0])+": "+str(x[1]), where_.iteritems())) )
+ bottle.abort(httperrors.Not_Found, "No netmap found with " + " and ".join(map(lambda x: str(x[0])+": "+str(x[1]), where_.iteritems())) )
if netmap_id:
return format_out({"result": "netmap %s deleted" % netmap_id})
else:
@@ -859,7 +680,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/netmaps/upload', method='POST')
@@ -867,7 +688,7 @@
logger.debug('FROM %s %s %s', bottle.request.remote_addr, bottle.request.method, bottle.request.url)
try:
netmaps = nfvo.datacenter_new_netmap(mydb, tenant_id, datacenter_id, None)
- convert_datetime2str(netmaps)
+ utils.convert_float_timestamp2str(netmaps)
utils.convert_str2boolean(netmaps, ('shared', 'multipoint') )
data={'netmaps' : netmaps}
return format_out(data)
@@ -878,7 +699,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/netmaps', method='POST')
@@ -893,7 +714,7 @@
try:
#obtain data, check that only one exist
netmaps = nfvo.datacenter_new_netmap(mydb, tenant_id, datacenter_id, http_content)
- convert_datetime2str(netmaps)
+ utils.convert_float_timestamp2str(netmaps)
utils.convert_str2boolean(netmaps, ('shared', 'multipoint') )
data={'netmaps' : netmaps}
return format_out(data)
@@ -904,7 +725,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/netmaps/<netmap_id>', method='PUT')
@@ -916,7 +737,7 @@
r = utils.remove_extra_items(http_content, netmap_edit_schema)
if r:
logger.debug("Remove received extra items %s", str(r))
-
+
#obtain data, check that only one exist
try:
nfvo.datacenter_edit_netmap(mydb, tenant_id, datacenter_id, netmap_id, http_content)
@@ -928,8 +749,8 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
-
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
+
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>/action', method='POST')
def http_action_datacenter_id(tenant_id, datacenter_id):
@@ -954,13 +775,13 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/datacenters/<datacenter_id>', method='DELETE')
def http_delete_datacenter_id( datacenter_id):
'''delete a tenant from database, can use both uuid or name'''
-
+
logger.debug('FROM %s %s %s', bottle.request.remote_addr, bottle.request.method, bottle.request.url)
try:
data = nfvo.delete_datacenter(mydb, datacenter_id)
@@ -972,7 +793,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>', method='POST')
@@ -996,7 +817,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vim_accounts/<vim_account_id>', method='PUT')
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>', method='PUT')
@@ -1019,7 +840,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/datacenters/<datacenter_id>', method='DELETE')
@@ -1037,7 +858,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vim/<datacenter_id>/network/<network_id>/attach', method='POST')
def http_post_vim_net_sdn_attach(tenant_id, datacenter_id, network_id):
@@ -1053,7 +874,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vim/<datacenter_id>/network/<network_id>/detach', method='DELETE')
@bottle.route(url_base + '/<tenant_id>/vim/<datacenter_id>/network/<network_id>/detach/<port_id>', method='DELETE')
@@ -1069,8 +890,8 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
-
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
+
@bottle.route(url_base + '/<tenant_id>/vim/<datacenter_id>/<item>', method='GET')
@bottle.route(url_base + '/<tenant_id>/vim/<datacenter_id>/<item>/<name>', method='GET')
def http_get_vim_items(tenant_id, datacenter_id, item, name=None):
@@ -1085,7 +906,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vim/<datacenter_id>/<item>/<name>', method='DELETE')
@@ -1101,7 +922,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vim/<datacenter_id>/<item>', method='POST')
@@ -1118,7 +939,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vnfs', method='GET')
@@ -1135,7 +956,7 @@
vnfs = mydb.get_rows(FROM='vnfs', SELECT=select_, WHERE=where_, LIMIT=limit_)
# change_keys_http2db(content, http2db_vnf, reverse=True)
utils.convert_str2boolean(vnfs, ('public',))
- convert_datetime2str(vnfs)
+ utils.convert_float_timestamp2str(vnfs)
data={'vnfs': vnfs}
return format_out(data)
except bottle.HTTPError:
@@ -1145,7 +966,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vnfs/<vnf_id>', method='GET')
@@ -1155,7 +976,7 @@
try:
vnf = nfvo.get_vnf_id(mydb,tenant_id,vnf_id)
utils.convert_str2boolean(vnf, ('public',))
- convert_datetime2str(vnf)
+ utils.convert_float_timestamp2str(vnf)
return format_out(vnf)
except bottle.HTTPError:
raise
@@ -1164,7 +985,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vnfs', method='POST')
@@ -1187,7 +1008,7 @@
vnf_id = nfvo.new_vnf_v02(mydb,tenant_id,http_content)
else:
logger.warning('Unexpected schema_version: %s', http_content.get("schema_version"))
- bottle.abort(HTTP_Bad_Request, "Invalid schema version")
+ bottle.abort(httperrors.Bad_Request, "Invalid schema version")
return http_get_vnf_id(tenant_id, vnf_id)
except bottle.HTTPError:
raise
@@ -1196,7 +1017,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/v3/<tenant_id>/vnfd', method='POST')
@@ -1214,7 +1035,7 @@
for vnfd_uuid in vnfd_uuid_list:
vnf = nfvo.get_vnf_id(mydb, tenant_id, vnfd_uuid)
utils.convert_str2boolean(vnf, ('public',))
- convert_datetime2str(vnf)
+ utils.convert_float_timestamp2str(vnf)
vnfd_list.append(vnf["vnf"])
return format_out({"vnfd": vnfd_list})
except bottle.HTTPError:
@@ -1224,13 +1045,13 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/vnfs/<vnf_id>', method='DELETE')
def http_delete_vnf_id(tenant_id, vnf_id):
'''delete a vnf from database, and images and flavors in VIM when appropriate, can use both uuid or name'''
logger.debug('FROM %s %s %s', bottle.request.remote_addr, bottle.request.method, bottle.request.url)
- #check valid tenant_id and deletes the vnf, including images,
+ #check valid tenant_id and deletes the vnf, including images,
try:
data = nfvo.delete_vnf(mydb,tenant_id,vnf_id)
#print json.dumps(data, indent=4)
@@ -1242,7 +1063,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
#@bottle.route(url_base + '/<tenant_id>/hosts/topology', method='GET')
@@ -1258,12 +1079,12 @@
else:
#openmano-gui is using a hardcoded value for the datacenter
result, data = nfvo.get_hosts_info(mydb, tenant_id) #, datacenter)
-
+
if result < 0:
#print "http_get_hosts error %d %s" % (-result, data)
bottle.abort(-result, data)
else:
- convert_datetime2str(data)
+ utils.convert_float_timestamp2str(data)
#print json.dumps(data, indent=4)
return format_out(data)
except bottle.HTTPError:
@@ -1273,7 +1094,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<path:path>', method='OPTIONS')
@@ -1297,7 +1118,7 @@
#r = utils.remove_extra_items(http_content, used_schema)
#if r is not None: print "http_post_deploy: Warning: remove extra items ", r
#print "http_post_deploy input: ", http_content
-
+
try:
scenario_id = nfvo.new_scenario(mydb, tenant_id, http_content)
instance = nfvo.start_scenario(mydb, tenant_id, scenario_id, http_content['name'], http_content['name'])
@@ -1310,7 +1131,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/topology/verify', method='POST')
@@ -1319,7 +1140,7 @@
# '''post topology verify'''
# print "http_post_verify by tenant " + tenant_id + ' datacenter ' + datacenter
logger.debug('FROM %s %s %s', bottle.request.remote_addr, bottle.request.method, bottle.request.url)
- return
+ return
#
# SCENARIOS
@@ -1342,7 +1163,7 @@
scenario_id = nfvo.new_scenario_v02(mydb, tenant_id, http_content, "0.3")
else:
logger.warning('Unexpected schema_version: %s', http_content.get("schema_version"))
- bottle.abort(HTTP_Bad_Request, "Invalid schema version")
+ bottle.abort(httperrors.Bad_Request, "Invalid schema version")
#print json.dumps(data, indent=4)
#return format_out(data)
return http_get_scenario_id(tenant_id, scenario_id)
@@ -1353,7 +1174,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/v3/<tenant_id>/nsd', method='POST')
def http_post_nsds_v3(tenant_id):
@@ -1369,7 +1190,7 @@
nsd_list = []
for nsd_uuid in nsd_uuid_list:
scenario = mydb.get_scenario(nsd_uuid, tenant_id)
- convert_datetime2str(scenario)
+ utils.convert_float_timestamp2str(scenario)
nsd_list.append(scenario)
data = {'nsd': nsd_list}
return format_out(data)
@@ -1380,7 +1201,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/scenarios/<scenario_id>/action', method='POST')
@@ -1424,7 +1245,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/scenarios', method='GET')
@@ -1434,14 +1255,14 @@
try:
#check valid tenant_id
if tenant_id != "any":
- nfvo.check_tenant(mydb, tenant_id)
+ nfvo.check_tenant(mydb, tenant_id)
#obtain data
s,w,l=filter_query_string(bottle.request.query, None,
('uuid', 'name', 'osm_id', 'description', 'tenant_id', 'created_at', 'public'))
if tenant_id != "any":
w["OR"] = {"tenant_id": tenant_id, "public": True}
scenarios = mydb.get_rows(SELECT=s, WHERE=w, LIMIT=l, FROM='scenarios')
- convert_datetime2str(scenarios)
+ utils.convert_float_timestamp2str(scenarios)
utils.convert_str2boolean(scenarios, ('public',) )
data={'scenarios':scenarios}
#print json.dumps(scenarios, indent=4)
@@ -1453,7 +1274,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/scenarios/<scenario_id>', method='GET')
@@ -1463,10 +1284,10 @@
try:
#check valid tenant_id
if tenant_id != "any":
- nfvo.check_tenant(mydb, tenant_id)
+ nfvo.check_tenant(mydb, tenant_id)
#obtain data
scenario = mydb.get_scenario(scenario_id, tenant_id)
- convert_datetime2str(scenario)
+ utils.convert_float_timestamp2str(scenario)
data={'scenario' : scenario}
return format_out(data)
except bottle.HTTPError:
@@ -1476,7 +1297,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/scenarios/<scenario_id>', method='DELETE')
@@ -1498,7 +1319,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/scenarios/<scenario_id>', method='PUT')
@@ -1521,7 +1342,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/instances', method='POST')
def http_post_instances(tenant_id):
@@ -1535,17 +1356,17 @@
try:
#check valid tenant_id
if tenant_id != "any":
- nfvo.check_tenant(mydb, tenant_id)
+ nfvo.check_tenant(mydb, tenant_id)
data = nfvo.create_instance(mydb, tenant_id, http_content["instance"])
return format_out(data)
except bottle.HTTPError:
raise
except (nfvo.NfvoException, db_base_Exception) as e:
- logger.error("http_post_instances error {}: {}".format(e.http_code, str(e)))
+ logger.error("http_post_instances error {}: {}".format(e.http_code, str(e)), exc_info=True)
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
#
# INSTANCES
@@ -1556,13 +1377,13 @@
try:
#check valid tenant_id
if tenant_id != "any":
- nfvo.check_tenant(mydb, tenant_id)
+ nfvo.check_tenant(mydb, tenant_id)
#obtain data
s,w,l=filter_query_string(bottle.request.query, None, ('uuid', 'name', 'scenario_id', 'tenant_id', 'description', 'created_at'))
if tenant_id != "any":
w['tenant_id'] = tenant_id
instances = mydb.get_rows(SELECT=s, WHERE=w, LIMIT=l, FROM='instance_scenarios')
- convert_datetime2str(instances)
+ utils.convert_float_timestamp2str(instances)
utils.convert_str2boolean(instances, ('public',) )
data={'instances':instances}
return format_out(data)
@@ -1573,7 +1394,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/instances/<instance_id>', method='GET')
@@ -1584,7 +1405,7 @@
#check valid tenant_id
if tenant_id != "any":
- nfvo.check_tenant(mydb, tenant_id)
+ nfvo.check_tenant(mydb, tenant_id)
if tenant_id == "any":
tenant_id = None
@@ -1598,7 +1419,7 @@
index = iface["ip_address"].find(";")
if index >= 0:
iface["ip_address"] = iface["ip_address"][:index]
- convert_datetime2str(instance)
+ utils.convert_float_timestamp2str(instance)
# print json.dumps(instance, indent=4)
return format_out(instance)
except bottle.HTTPError:
@@ -1608,7 +1429,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/instances/<instance_id>', method='DELETE')
@@ -1618,7 +1439,7 @@
try:
#check valid tenant_id
if tenant_id != "any":
- nfvo.check_tenant(mydb, tenant_id)
+ nfvo.check_tenant(mydb, tenant_id)
if tenant_id == "any":
tenant_id = None
#obtain data
@@ -1631,7 +1452,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/instances/<instance_id>/action', method='POST')
@@ -1651,13 +1472,13 @@
try:
#check valid tenant_id
if tenant_id != "any":
- nfvo.check_tenant(mydb, tenant_id)
+ nfvo.check_tenant(mydb, tenant_id)
#print "http_post_instance_scenario_action input: ", http_content
#obtain data
instance = mydb.get_instance_scenario(instance_id, tenant_id)
instance_id = instance["uuid"]
-
+
data = nfvo.instance_action(mydb, tenant_id, instance_id, http_content)
return format_out(data)
except bottle.HTTPError:
@@ -1667,7 +1488,7 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
@bottle.route(url_base + '/<tenant_id>/instances/<instance_id>/action', method='GET')
@@ -1693,34 +1514,17 @@
bottle.abort(e.http_code, str(e))
except Exception as e:
logger.error("Unexpected exception: ", exc_info=True)
- bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
+ bottle.abort(httperrors.Internal_Server_Error, type(e).__name__ + ": " + str(e))
-def remove_clear_passwd(data):
- """
- Removes clear passwords from the data received
- :param data: data with clear password
- :return: data without the password information
- """
-
- passw = ['password: ', 'passwd: ']
-
- for pattern in passw:
- init = data.find(pattern)
- while init != -1:
- end = data.find('\n', init)
- data = data[:init] + '{}******'.format(pattern) + data[end:]
- init += 1
- init = data.find(pattern, init)
- return data
@bottle.error(400)
-@bottle.error(401)
-@bottle.error(404)
+@bottle.error(401)
+@bottle.error(404)
@bottle.error(403)
-@bottle.error(405)
+@bottle.error(405)
@bottle.error(406)
@bottle.error(409)
-@bottle.error(503)
+@bottle.error(503)
@bottle.error(500)
def error400(error):
e={"error":{"code":error.status_code, "type":error.status, "description":error.body}}
diff --git a/osm_ro/nfvo.py b/osm_ro/nfvo.py
index 0c8cef6..7b8a820 100644
--- a/osm_ro/nfvo.py
+++ b/osm_ro/nfvo.py
@@ -28,13 +28,11 @@
__date__ ="$16-sep-2014 22:05:01$"
# import imp
-# import json
+import json
import yaml
import utils
from utils import deprecated
import vim_thread
-from db_base import HTTP_Unauthorized, HTTP_Bad_Request, HTTP_Internal_Server_Error, HTTP_Not_Found,\
- HTTP_Conflict, HTTP_Method_Not_Allowed
import console_proxy_thread as cli
import vimconn
import logging
@@ -56,8 +54,22 @@
from copy import deepcopy
+# WIM
+import wim.wimconn as wimconn
+import wim.wim_thread as wim_thread
+from .http_tools import errors as httperrors
+from .wim.engine import WimEngine
+from .wim.persistence import WimPersistence
+from copy import deepcopy
+#
+
global global_config
global vimconn_imported
+# WIM
+global wim_engine
+wim_engine = None
+global wimconn_imported
+#
global logger
global default_volume_size
default_volume_size = '5' #size in GB
@@ -68,16 +80,21 @@
vimconn_imported = {} # dictionary with VIM type as key, loaded module as value
vim_threads = {"running":{}, "deleting": {}, "names": []} # threads running for attached-VIMs
vim_persistent_info = {}
+# WIM
+wimconn_imported = {} # dictionary with WIM type as key, loaded module as value
+wim_threads = {"running":{}, "deleting": {}, "names": []} # threads running for attached-WIMs
+wim_persistent_info = {}
+#
+
logger = logging.getLogger('openmano.nfvo')
task_lock = Lock()
last_task_id = 0.0
db = None
db_lock = Lock()
-class NfvoException(Exception):
- def __init__(self, message, http_code):
- self.http_code = http_code
- Exception.__init__(self, message)
+
+class NfvoException(httperrors.HttpMappedError):
+ """Common Class for NFVO errors"""
def get_task_id():
@@ -116,13 +133,32 @@
vim_threads["names"].append(name)
return name
+# -- Move
+def get_non_used_wim_name(wim_name, wim_id, tenant_name, tenant_id):
+ name = wim_name[:16]
+ if name not in wim_threads["names"]:
+ wim_threads["names"].append(name)
+ return name
+ name = wim_name[:16] + "." + tenant_name[:16]
+ if name not in wim_threads["names"]:
+ wim_threads["names"].append(name)
+ return name
+ name = wim_id + "-" + tenant_id
+ wim_threads["names"].append(name)
+ return name
-def start_service(mydb):
+
+def start_service(mydb, persistence=None, wim=None):
global db, global_config
db = nfvo_db.nfvo_db()
db.connect(global_config['db_host'], global_config['db_user'], global_config['db_passwd'], global_config['db_name'])
global ovim
+ if persistence:
+ persistence.lock = db_lock
+ else:
+ persistence = WimPersistence(db, lock=db_lock)
+
# Initialize openvim for SDN control
# TODO: Avoid static configuration by adding new parameters to openmanod.cfg
# TODO: review ovim.py to delete not needed configuration
@@ -143,9 +179,14 @@
try:
# starts ovim library
ovim = ovim_module.ovim(ovim_configuration)
+
+ global wim_engine
+ wim_engine = wim or WimEngine(persistence)
+ wim_engine.ovim = ovim
+
ovim.start_service()
- #delete old unneeded vim_actions
+ #delete old unneeded vim_wim_actions
clean_db(mydb)
# starts vim_threads
@@ -176,13 +217,13 @@
# if module_info and module_info[0]:
# file.close(module_info[0])
raise NfvoException("Unknown vim type '{}'. Cannot open file '{}.py'; {}: {}".format(
- vim["type"], module, type(e).__name__, str(e)), HTTP_Bad_Request)
+ vim["type"], module, type(e).__name__, str(e)), httperrors.Bad_Request)
thread_id = vim['datacenter_tenant_id']
vim_persistent_info[thread_id] = {}
try:
#if not tenant:
- # return -HTTP_Bad_Request, "You must provide a valid tenant name or uuid for VIM %s" % ( vim["type"])
+ # return -httperrors.Bad_Request, "You must provide a valid tenant name or uuid for VIM %s" % ( vim["type"])
myvim = vimconn_imported[ vim["type"] ].vimconnector(
uuid=vim['datacenter_id'], name=vim['datacenter_name'],
tenant_id=vim['vim_tenant_id'], tenant_name=vim['vim_tenant_name'],
@@ -196,13 +237,15 @@
vim['datacenter_id'], e))
except Exception as e:
raise NfvoException("Error at VIM {}; {}: {}".format(vim["type"], type(e).__name__, e),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
thread_name = get_non_used_vim_name(vim['datacenter_name'], vim['vim_tenant_id'], vim['vim_tenant_name'],
vim['vim_tenant_id'])
new_thread = vim_thread.vim_thread(task_lock, thread_name, vim['datacenter_name'],
vim['datacenter_tenant_id'], db=db, db_lock=db_lock, ovim=ovim)
new_thread.start()
vim_threads["running"][thread_id] = new_thread
+
+ wim_engine.start_threads()
except db_base_Exception as e:
raise NfvoException(str(e) + " at nfvo.get_vim", e.http_code)
except ovim_module.ovimException as e:
@@ -213,17 +256,21 @@
msg=message[22:-3], dbname=global_config["db_ovim_name"],
dbuser=global_config["db_ovim_user"], dbpass=global_config["db_ovim_passwd"],
ver=message[-3:-1], dbhost=global_config["db_ovim_host"])
- raise NfvoException(message, HTTP_Bad_Request)
+ raise NfvoException(message, httperrors.Bad_Request)
def stop_service():
global ovim, global_config
if ovim:
ovim.stop_service()
- for thread_id,thread in vim_threads["running"].items():
+ for thread_id, thread in vim_threads["running"].items():
thread.insert_task("exit")
vim_threads["deleting"][thread_id] = thread
vim_threads["running"] = {}
+
+ if wim_engine:
+ wim_engine.stop_threads()
+
if global_config and global_config.get("console_thread"):
for thread in global_config["console_thread"]:
thread.terminate = True
@@ -238,21 +285,21 @@
:param mydb: database connector
:return: None
"""
- # get and delete unused vim_actions: all elements deleted, one week before, instance not present
+ # get and delete unused vim_wim_actions: all elements deleted, one week before, instance not present
now = t.time()-3600*24*7
instance_action_id = None
nb_deleted = 0
while True:
actions_to_delete = mydb.get_rows(
SELECT=("item", "item_id", "instance_action_id"),
- FROM="vim_actions as va join instance_actions as ia on va.instance_action_id=ia.uuid "
+ FROM="vim_wim_actions as va join instance_actions as ia on va.instance_action_id=ia.uuid "
"left join instance_scenarios as i on ia.instance_id=i.uuid",
WHERE={"va.action": "DELETE", "va.modified_at<": now, "i.uuid": None,
"va.status": ("DONE", "SUPERSEDED")},
LIMIT=100
)
for to_delete in actions_to_delete:
- mydb.delete_row(FROM="vim_actions", WHERE=to_delete)
+ mydb.delete_row(FROM="vim_wim_actions", WHERE=to_delete)
if instance_action_id != to_delete["instance_action_id"]:
instance_action_id = to_delete["instance_action_id"]
mydb.delete_row(FROM="instance_actions", WHERE={"uuid": instance_action_id})
@@ -260,8 +307,7 @@
if len(actions_to_delete) < 100:
break
if nb_deleted:
- logger.debug("Removed {} unused vim_actions".format(nb_deleted))
-
+ logger.debug("Removed {} unused vim_wim_actions".format(nb_deleted))
def get_flavorlist(mydb, vnf_id, nfvo_tenant=None):
@@ -357,7 +403,7 @@
vim["type"], module, type(e).__name__, str(e)))
continue
raise NfvoException("Unknown vim type '{}'. Can not open file '{}.py'; {}: {}".format(
- vim["type"], module, type(e).__name__, str(e)), HTTP_Bad_Request)
+ vim["type"], module, type(e).__name__, str(e)), httperrors.Bad_Request)
try:
if 'datacenter_tenant_id' in vim:
@@ -368,7 +414,7 @@
else:
persistent_info = {}
#if not tenant:
- # return -HTTP_Bad_Request, "You must provide a valid tenant name or uuid for VIM %s" % ( vim["type"])
+ # return -httperrors.Bad_Request, "You must provide a valid tenant name or uuid for VIM %s" % ( vim["type"])
vim_dict[ vim['datacenter_id'] ] = vimconn_imported[ vim["type"] ].vimconnector(
uuid=vim['datacenter_id'], name=vim['datacenter_name'],
tenant_id=vim.get('vim_tenant_id',vim_tenant),
@@ -381,7 +427,7 @@
if ignore_errors:
logger.error("Error at VIM {}; {}: {}".format(vim["type"], type(e).__name__, str(e)))
continue
- http_code = HTTP_Internal_Server_Error
+ http_code = httperrors.Internal_Server_Error
if isinstance(e, vimconn.vimconnException):
http_code = e.http_code
raise NfvoException("Error at VIM {}; {}: {}".format(vim["type"], type(e).__name__, str(e)), http_code)
@@ -446,7 +492,7 @@
raise NfvoException(
"Error at vnf:VNFC[name:'{}']:numas:interfaces:name, interface name '{}' already used in this VNFC".format(
vnfc["name"], interface["name"]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
name_dict[ interface["name"] ] = "underlay"
#bridge interfaces
for interface in vnfc.get("bridge-ifaces",() ):
@@ -454,7 +500,7 @@
raise NfvoException(
"Error at vnf:VNFC[name:'{}']:bridge-ifaces:name, interface name '{}' already used in this VNFC".format(
vnfc["name"], interface["name"]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
name_dict[ interface["name"] ] = "overlay"
vnfc_interfaces[ vnfc["name"] ] = name_dict
# check bood-data info
@@ -463,7 +509,7 @@
# if (vnfc["boot-data"].get("users") or vnfc["boot-data"].get("config-files")) and vnfc["boot-data"].get("user-data"):
# raise NfvoException(
# "Error at vnf:VNFC:boot-data, fields 'users' and 'config-files' are not compatible with 'user-data'",
- # HTTP_Bad_Request)
+ # httperrors.Bad_Request)
#check if the info in external_connections matches with the one in the vnfcs
name_list=[]
@@ -472,20 +518,20 @@
raise NfvoException(
"Error at vnf:external-connections:name, value '{}' already used as an external-connection".format(
external_connection["name"]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
name_list.append(external_connection["name"])
if external_connection["VNFC"] not in vnfc_interfaces:
raise NfvoException(
"Error at vnf:external-connections[name:'{}']:VNFC, value '{}' does not match any VNFC".format(
external_connection["name"], external_connection["VNFC"]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
if external_connection["local_iface_name"] not in vnfc_interfaces[ external_connection["VNFC"] ]:
raise NfvoException(
"Error at vnf:external-connections[name:'{}']:local_iface_name, value '{}' does not match any interface of this VNFC".format(
external_connection["name"],
external_connection["local_iface_name"]),
- HTTP_Bad_Request )
+ httperrors.Bad_Request )
#check if the info in internal_connections matches with the one in the vnfcs
name_list=[]
@@ -494,7 +540,7 @@
raise NfvoException(
"Error at vnf:internal-connections:name, value '%s' already used as an internal-connection".format(
internal_connection["name"]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
name_list.append(internal_connection["name"])
#We should check that internal-connections of type "ptp" have only 2 elements
@@ -504,7 +550,7 @@
internal_connection["name"],
'ptp' if vnf_descriptor_version==1 else 'e-line',
'data' if vnf_descriptor_version==1 else "e-lan"),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
for port in internal_connection["elements"]:
vnf = port["VNFC"]
iface = port["local_iface_name"]
@@ -512,13 +558,13 @@
raise NfvoException(
"Error at vnf:internal-connections[name:'{}']:elements[]:VNFC, value '{}' does not match any VNFC".format(
internal_connection["name"], vnf),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
if iface not in vnfc_interfaces[ vnf ]:
raise NfvoException(
"Error at vnf:internal-connections[name:'{}']:elements[]:local_iface_name, value '{}' does not match any interface of this VNFC".format(
internal_connection["name"], iface),
- HTTP_Bad_Request)
- return -HTTP_Bad_Request,
+ httperrors.Bad_Request)
+ return -httperrors.Bad_Request,
if vnf_descriptor_version==1 and "type" not in internal_connection:
if vnfc_interfaces[vnf][iface] == "overlay":
internal_connection["type"] = "bridge"
@@ -536,7 +582,7 @@
internal_connection["name"],
iface, 'bridge' if vnf_descriptor_version==1 else 'overlay',
'data' if vnf_descriptor_version==1 else 'underlay'),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
if (internal_connection.get("type") == "bridge" or internal_connection.get("implementation") == "overlay") and \
vnfc_interfaces[vnf][iface] == "underlay":
raise NfvoException(
@@ -544,7 +590,7 @@
internal_connection["name"], iface,
'data' if vnf_descriptor_version==1 else 'underlay',
'bridge' if vnf_descriptor_version==1 else 'overlay'),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
def create_or_use_image(mydb, vims, image_dict, rollback_list, only_create_at_vim=False, return_on_error=None):
@@ -589,7 +635,7 @@
vim_images = vim.get_image_list(filter_dict)
#logger.debug('>>>>>>>> VIM images: %s', str(vim_images))
if len(vim_images) > 1:
- raise vimconn.vimconnException("More than one candidate VIM image found for filter: {}".format(str(filter_dict)), HTTP_Conflict)
+ raise vimconn.vimconnException("More than one candidate VIM image found for filter: {}".format(str(filter_dict)), httperrors.Conflict)
elif len(vim_images) == 0:
raise vimconn.vimconnNotFoundException("Image not found at VIM with filter: '{}'".format(str(filter_dict)))
else:
@@ -845,7 +891,7 @@
try:
pybindJSONDecoder.load_ietf_json(vnf_descriptor, None, None, obj=myvnfd, path_helper=True)
except Exception as e:
- raise NfvoException("Error. Invalid VNF descriptor format " + str(e), HTTP_Bad_Request)
+ raise NfvoException("Error. Invalid VNF descriptor format " + str(e), httperrors.Bad_Request)
db_vnfs = []
db_nets = []
db_vms = []
@@ -929,7 +975,7 @@
raise NfvoException("Error. Invalid VNF descriptor at 'vnfd[{}]':'vld[{}]':'ip-profile-ref':"
"'{}'. Reference to a non-existing 'ip_profiles'".format(
str(vnfd["id"]), str(vld["id"]), str(vld["ip-profile-ref"])),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
db_ip_profiles[ip_profile_name2db_table_index[ip_profile_name]]["net_id"] = net_uuid
else: #check no ip-address has been defined
for icp in vld.get("internal-connection-point").itervalues():
@@ -937,7 +983,7 @@
raise NfvoException("Error at 'vnfd[{}]':'vld[{}]':'internal-connection-point[{}]' "
"contains an ip-address but no ip-profile has been defined at VLD".format(
str(vnfd["id"]), str(vld["id"]), str(icp["id"])),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
# connection points vaiable declaration
cp_name2iface_uuid = {}
@@ -1086,7 +1132,7 @@
raise NfvoException("Error. Invalid VNF descriptor at 'vnfd[{}]':'vdu[{}]':'interface':'virtual"
"-interface':'type':'{}'. Interface type is not supported".format(
vnfd_id, vdu_id, iface.get("virtual-interface").get("type")),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
if iface.get("mgmt-interface"):
db_interface["type"] = "mgmt"
@@ -1120,7 +1166,7 @@
" at connection-point".format(
vnf=vnfd_id, vdu=vdu_id, iface=iface["name"],
cp=iface.get("vnfd-connection-point-ref")),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
elif iface.get("internal-connection-point-ref"):
try:
for icp_descriptor in vdu_descriptor["internal-connection-point"]:
@@ -1155,7 +1201,7 @@
" {msg}".format(
vnf=vnfd_id, vdu=vdu_id, iface=iface["name"],
cp=iface.get("internal-connection-point-ref"), msg=str(e)),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
if iface.get("position"):
db_interface["created_at"] = int(iface.get("position")) * 50
if iface.get("mac-address"):
@@ -1240,12 +1286,12 @@
raise NfvoException("Error. Invalid VNF descriptor at 'vnfd[{vnf}]':'placement-groups[{pg}]':"
"'member-vdus':'{vdu}'. Reference to a non-existing vdu".format(
vnf=vnfd_id, pg=pg_name, vdu=vdu_id),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
if vdu_id2db_table_index[vdu_id]:
db_vms[vdu_id2db_table_index[vdu_id]]["availability_zone"] = pg_name
# TODO consider the case of isolation and not colocation
# if pg.get("strategy") == "ISOLATION":
-
+
# VNF mgmt configuration
mgmt_access = {}
if vnfd["mgmt-interface"].get("vdu-id"):
@@ -1254,7 +1300,7 @@
raise NfvoException("Error. Invalid VNF descriptor at 'vnfd[{vnf}]':'mgmt-interface':'vdu-id':"
"'{vdu}'. Reference to a non-existing vdu".format(
vnf=vnfd_id, vdu=mgmt_vdu_id),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
mgmt_access["vm_id"] = vdu_id2uuid[vnfd["mgmt-interface"]["vdu-id"]]
# if only one cp is defined by this VDU, mark this interface as of type "mgmt"
if vdu_id2cp_name.get(mgmt_vdu_id):
@@ -1268,7 +1314,7 @@
raise NfvoException("Error. Invalid VNF descriptor at 'vnfd[{vnf}]':'mgmt-interface':'cp'['{cp}']. "
"Reference to a non-existing connection-point".format(
vnf=vnfd_id, cp=vnfd["mgmt-interface"]["cp"]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
mgmt_access["vm_id"] = cp_name2vm_uuid[vnfd["mgmt-interface"]["cp"]]
mgmt_access["interface_id"] = cp_name2iface_uuid[vnfd["mgmt-interface"]["cp"]]
# mark this interface as of type mgmt
@@ -1307,7 +1353,7 @@
raise
except Exception as e:
logger.error("Exception {}".format(e))
- raise # NfvoException("Exception {}".format(e), HTTP_Bad_Request)
+ raise # NfvoException("Exception {}".format(e), httperrors.Bad_Request)
@deprecated("Use new_vnfd_v3")
@@ -1323,7 +1369,7 @@
if "tenant_id" in vnf_descriptor["vnf"]:
if vnf_descriptor["vnf"]["tenant_id"] != tenant_id:
raise NfvoException("VNF can not have a different tenant owner '{}', must be '{}'".format(vnf_descriptor["vnf"]["tenant_id"], tenant_id),
- HTTP_Unauthorized)
+ httperrors.Unauthorized)
else:
vnf_descriptor['vnf']['tenant_id'] = tenant_id
# Step 3. Get the URL of the VIM from the nfvo_tenant and the datacenter
@@ -1381,9 +1427,9 @@
# result2, message = rollback(myvim, myvimURL, myvim_tenant, flavorList, imageList)
# if result2:
# print "Error creating flavor: unknown processor model. Rollback successful."
- # return -HTTP_Bad_Request, "Error creating flavor: unknown processor model. Rollback successful."
+ # return -httperrors.Bad_Request, "Error creating flavor: unknown processor model. Rollback successful."
# else:
- # return -HTTP_Bad_Request, "Error creating flavor: unknown processor model. Rollback fail: you need to access VIM and delete the following %s" % message
+ # return -httperrors.Bad_Request, "Error creating flavor: unknown processor model. Rollback fail: you need to access VIM and delete the following %s" % message
myflavorDict['extended']['processor_ranking'] = 100 #Hardcoded value, while we decide when the mapping is done
if 'numas' in vnfc and len(vnfc['numas'])>0:
@@ -1440,7 +1486,7 @@
error_text = "Exception at database"
elif isinstance(e, KeyError):
error_text = "KeyError exception "
- e.http_code = HTTP_Internal_Server_Error
+ e.http_code = httperrors.Internal_Server_Error
else:
error_text = "Exception at VIM"
error_text += " {} {}. {}".format(type(e).__name__, str(e), message)
@@ -1461,7 +1507,7 @@
if "tenant_id" in vnf_descriptor["vnf"]:
if vnf_descriptor["vnf"]["tenant_id"] != tenant_id:
raise NfvoException("VNF can not have a different tenant owner '{}', must be '{}'".format(vnf_descriptor["vnf"]["tenant_id"], tenant_id),
- HTTP_Unauthorized)
+ httperrors.Unauthorized)
else:
vnf_descriptor['vnf']['tenant_id'] = tenant_id
# Step 3. Get the URL of the VIM from the nfvo_tenant and the datacenter
@@ -1518,9 +1564,9 @@
# result2, message = rollback(myvim, myvimURL, myvim_tenant, flavorList, imageList)
# if result2:
# print "Error creating flavor: unknown processor model. Rollback successful."
- # return -HTTP_Bad_Request, "Error creating flavor: unknown processor model. Rollback successful."
+ # return -httperrors.Bad_Request, "Error creating flavor: unknown processor model. Rollback successful."
# else:
- # return -HTTP_Bad_Request, "Error creating flavor: unknown processor model. Rollback fail: you need to access VIM and delete the following %s" % message
+ # return -httperrors.Bad_Request, "Error creating flavor: unknown processor model. Rollback fail: you need to access VIM and delete the following %s" % message
myflavorDict['extended']['processor_ranking'] = 100 #Hardcoded value, while we decide when the mapping is done
if 'numas' in vnfc and len(vnfc['numas'])>0:
@@ -1576,7 +1622,7 @@
error_text = "Exception at database"
elif isinstance(e, KeyError):
error_text = "KeyError exception "
- e.http_code = HTTP_Internal_Server_Error
+ e.http_code = httperrors.Internal_Server_Error
else:
error_text = "Exception at VIM"
error_text += " {} {}. {}".format(type(e).__name__, str(e), message)
@@ -1605,7 +1651,7 @@
'boot_data'),
WHERE={'vnfs.uuid': vnf_id} )
if len(content) != 0:
- #raise NfvoException("vnf '{}' not found".format(vnf_id), HTTP_Not_Found)
+ #raise NfvoException("vnf '{}' not found".format(vnf_id), httperrors.Not_Found)
# change boot_data into boot-data
for vm in content:
if vm.get("boot_data"):
@@ -1629,7 +1675,7 @@
if len(ipprofiles)==1:
net["ip_profile"] = ipprofiles[0]
elif len(ipprofiles)>1:
- raise NfvoException("More than one ip-profile found with this criteria: net_id='{}'".format(net['uuid']), HTTP_Bad_Request)
+ raise NfvoException("More than one ip-profile found with this criteria: net_id='{}'".format(net['uuid']), httperrors.Bad_Request)
#TODO: For each net, GET its elements and relevant info per element (VNFC, iface, ip_address) and include them in the output.
@@ -1673,7 +1719,7 @@
deleted = mydb.delete_row_by_id('vnfs', vnf_id)
if deleted == 0:
- raise NfvoException("vnf '{}' not found".format(vnf_id), HTTP_Not_Found)
+ raise NfvoException("vnf '{}' not found".format(vnf_id), httperrors.Not_Found)
undeletedItems = []
for flavor in flavorList:
@@ -1755,7 +1801,7 @@
if result < 0:
return result, vims
elif result == 0:
- return -HTTP_Not_Found, "datacenter '%s' not found" % datacenter_name
+ return -httperrors.Not_Found, "datacenter '%s' not found" % datacenter_name
myvim = vims.values()[0]
result,servers = myvim.get_hosts_info()
if result < 0:
@@ -1767,10 +1813,10 @@
def get_hosts(mydb, nfvo_tenant_id):
vims = get_vim(mydb, nfvo_tenant_id)
if len(vims) == 0:
- raise NfvoException("No datacenter found for tenant '{}'".format(str(nfvo_tenant_id)), HTTP_Not_Found)
+ raise NfvoException("No datacenter found for tenant '{}'".format(str(nfvo_tenant_id)), httperrors.Not_Found)
elif len(vims)>1:
#print "nfvo.datacenter_action() error. Several datacenters found"
- raise NfvoException("More than one datacenters found, try to identify with uuid", HTTP_Conflict)
+ raise NfvoException("More than one datacenters found, try to identify with uuid", httperrors.Conflict)
myvim = vims.values()[0]
try:
hosts = myvim.get_hosts()
@@ -1812,7 +1858,7 @@
if "tenant_id" in topo:
if topo["tenant_id"] != tenant_id:
raise NfvoException("VNF can not have a different tenant owner '{}', must be '{}'".format(topo["tenant_id"], tenant_id),
- HTTP_Unauthorized)
+ httperrors.Unauthorized)
else:
tenant_id=None
@@ -1844,15 +1890,15 @@
error_text += " 'VNF model' " + vnf['VNF model']
where['name'] = vnf['VNF model']
if len(where) == 1:
- raise NfvoException("Descriptor need a 'vnf_id' or 'VNF model' field at " + error_pos, HTTP_Bad_Request)
+ raise NfvoException("Descriptor need a 'vnf_id' or 'VNF model' field at " + error_pos, httperrors.Bad_Request)
vnf_db = mydb.get_rows(SELECT=('uuid','name','description'),
FROM='vnfs',
WHERE=where)
if len(vnf_db)==0:
- raise NfvoException("unknown" + error_text + " at " + error_pos, HTTP_Not_Found)
+ raise NfvoException("unknown" + error_text + " at " + error_pos, httperrors.Not_Found)
elif len(vnf_db)>1:
- raise NfvoException("more than one" + error_text + " at " + error_pos + " Concrete with 'vnf_id'", HTTP_Conflict)
+ raise NfvoException("more than one" + error_text + " at " + error_pos + " Concrete with 'vnf_id'", httperrors.Conflict)
vnf['uuid']=vnf_db[0]['uuid']
vnf['description']=vnf_db[0]['description']
#get external interfaces
@@ -1878,7 +1924,7 @@
con_type = conections[k].get("type", "link")
if con_type != "link":
if k in other_nets:
- raise NfvoException("Format error. Reapeted network name at 'topology':'connections':'{}'".format(str(k)), HTTP_Bad_Request)
+ raise NfvoException("Format error. Reapeted network name at 'topology':'connections':'{}'".format(str(k)), httperrors.Bad_Request)
other_nets[k] = {'external': False}
if conections[k].get("graph"):
other_nets[k]["graph"] = conections[k]["graph"]
@@ -1901,10 +1947,10 @@
for iface in ifaces_list:
if iface[0] not in vnfs and iface[0] not in other_nets :
raise NfvoException("format error. Invalid VNF name at 'topology':'connections':'{}':'nodes':'{}'".format(
- str(k), iface[0]), HTTP_Not_Found)
+ str(k), iface[0]), httperrors.Not_Found)
if iface[0] in vnfs and iface[1] not in vnfs[ iface[0] ]['ifaces']:
raise NfvoException("format error. Invalid interface name at 'topology':'connections':'{}':'nodes':'{}':'{}'".format(
- str(k), iface[0], iface[1]), HTTP_Not_Found)
+ str(k), iface[0], iface[1]), httperrors.Not_Found)
#1.5 unify connections from the pair list to a consolidated list
index=0
@@ -1941,13 +1987,13 @@
if 'name' not in net:
net['name']=k
if 'model' not in net:
- raise NfvoException("needed a 'model' at " + error_pos, HTTP_Bad_Request)
+ raise NfvoException("needed a 'model' at " + error_pos, httperrors.Bad_Request)
if net['model']=='bridge_net':
net['type']='bridge';
elif net['model']=='dataplane_net':
net['type']='data';
else:
- raise NfvoException("unknown 'model' '"+ net['model'] +"' at " + error_pos, HTTP_Not_Found)
+ raise NfvoException("unknown 'model' '"+ net['model'] +"' at " + error_pos, httperrors.Not_Found)
else: #external
#IF we do not want to check that external network exist at datacenter
pass
@@ -1961,17 +2007,17 @@
# error_text += " 'model' " + net['model']
# WHERE_['name'] = net['model']
# if len(WHERE_) == 0:
-# return -HTTP_Bad_Request, "needed a 'net_id' or 'model' at " + error_pos
+# return -httperrors.Bad_Request, "needed a 'net_id' or 'model' at " + error_pos
# r,net_db = mydb.get_table(SELECT=('uuid','name','description','type','shared'),
# FROM='datacenter_nets', WHERE=WHERE_ )
# if r<0:
# print "nfvo.new_scenario Error getting datacenter_nets",r,net_db
# elif r==0:
# print "nfvo.new_scenario Error" +error_text+ " is not present at database"
-# return -HTTP_Bad_Request, "unknown " +error_text+ " at " + error_pos
+# return -httperrors.Bad_Request, "unknown " +error_text+ " at " + error_pos
# elif r>1:
# print "nfvo.new_scenario Error more than one external_network for " +error_text+ " is present at database"
-# return -HTTP_Bad_Request, "more than one external_network for " +error_text+ "at "+ error_pos + " Concrete with 'net_id'"
+# return -httperrors.Bad_Request, "more than one external_network for " +error_text+ "at "+ error_pos + " Concrete with 'net_id'"
# other_nets[k].update(net_db[0])
#ENDIF
net_list={}
@@ -1988,7 +2034,7 @@
if other_net_index>=0:
error_text="There is some interface connected both to net '%s' and net '%s'" % (con[other_net_index][0], net_key)
#print "nfvo.new_scenario " + error_text
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
else:
other_net_index = index
net_target = net_key
@@ -2012,7 +2058,7 @@
# if type_=='data' and other_nets[net_target]['type']=="ptp":
# error_text = "Error connecting %d nodes on a not multipoint net %s" % (len(con), net_target)
# print "nfvo.new_scenario " + error_text
-# return -HTTP_Bad_Request, error_text
+# return -httperrors.Bad_Request, error_text
#ENDIF
for iface in con:
vnfs[ iface[0] ]['ifaces'][ iface[1] ]['net_key'] = net_target
@@ -2034,7 +2080,7 @@
if net_type_bridge and net_type_data:
error_text = "Error connection interfaces of bridge type with data type. Firs node %s, iface %s" % (iface[0], iface[1])
#print "nfvo.new_scenario " + error_text
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
elif net_type_bridge:
type_='bridge'
else:
@@ -2045,7 +2091,7 @@
error_text = "Error connection node %s : %s does not match any VNF or interface" % (iface[0], iface[1])
#print "nfvo.new_scenario " + error_text
#raise e
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
#1.8: Connect to management net all not already connected interfaces of type 'mgmt'
#1.8.1 obtain management net
@@ -2092,7 +2138,7 @@
if scenario["tenant_id"] != tenant_id:
# print "nfvo.new_scenario_v02() tenant '%s' not found" % tenant_id
raise NfvoException("VNF can not have a different tenant owner '{}', must be '{}'".format(
- scenario["tenant_id"], tenant_id), HTTP_Unauthorized)
+ scenario["tenant_id"], tenant_id), httperrors.Unauthorized)
else:
tenant_id=None
@@ -2108,14 +2154,14 @@
error_text += " 'vnf_name' " + vnf['vnf_name']
where['name'] = vnf['vnf_name']
if len(where) == 1:
- raise NfvoException("Needed a 'vnf_id' or 'vnf_name' at " + error_pos, HTTP_Bad_Request)
+ raise NfvoException("Needed a 'vnf_id' or 'vnf_name' at " + error_pos, httperrors.Bad_Request)
vnf_db = mydb.get_rows(SELECT=('uuid', 'name', 'description'),
FROM='vnfs',
WHERE=where)
if len(vnf_db) == 0:
- raise NfvoException("Unknown" + error_text + " at " + error_pos, HTTP_Not_Found)
+ raise NfvoException("Unknown" + error_text + " at " + error_pos, httperrors.Not_Found)
elif len(vnf_db) > 1:
- raise NfvoException("More than one" + error_text + " at " + error_pos + " Concrete with 'vnf_id'", HTTP_Conflict)
+ raise NfvoException("More than one" + error_text + " at " + error_pos + " Concrete with 'vnf_id'", httperrors.Conflict)
vnf['uuid'] = vnf_db[0]['uuid']
vnf['description'] = vnf_db[0]['description']
vnf['ifaces'] = {}
@@ -2143,17 +2189,17 @@
error_text = "Error at 'networks':'{}':'interfaces' VNF '{}' not match any VNF at 'vnfs'".format(
net_name, vnf)
# logger.debug("nfvo.new_scenario_v02 " + error_text)
- raise NfvoException(error_text, HTTP_Not_Found)
+ raise NfvoException(error_text, httperrors.Not_Found)
if iface not in scenario["vnfs"][vnf]['ifaces']:
error_text = "Error at 'networks':'{}':'interfaces':'{}' interface not match any VNF interface"\
.format(net_name, iface)
# logger.debug("nfvo.new_scenario_v02 " + error_text)
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
if "net_key" in scenario["vnfs"][vnf]['ifaces'][iface]:
error_text = "Error at 'networks':'{}':'interfaces':'{}' interface already connected at network"\
"'{}'".format(net_name, iface,scenario["vnfs"][vnf]['ifaces'][iface]['net_key'])
# logger.debug("nfvo.new_scenario_v02 " + error_text)
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
scenario["vnfs"][vnf]['ifaces'][ iface ]['net_key'] = net_name
scenario["vnfs"][vnf]['ifaces'][iface]['ip_address'] = ip_address
iface_type = scenario["vnfs"][vnf]['ifaces'][iface]['type']
@@ -2166,7 +2212,7 @@
error_text = "Error connection interfaces of 'bridge' type and 'data' type at 'networks':'{}':'interfaces'"\
.format(net_name)
# logger.debug("nfvo.new_scenario " + error_text)
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
elif net_type_bridge:
type_ = 'bridge'
else:
@@ -2177,19 +2223,19 @@
error_text = "Error connecting interfaces of data type to a network declared as 'underlay' at "\
"'network':'{}'".format(net_name)
# logger.debug(error_text)
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
elif type_ != "bridge" and net["implementation"] == "overlay":
error_text = "Error connecting interfaces of data type to a network declared as 'overlay' at "\
"'network':'{}'".format(net_name)
# logger.debug(error_text)
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
net.pop("implementation")
if "type" in net and version == "0.3": # for v0.3
if type_ == "data" and net["type"] == "e-line":
error_text = "Error connecting more than 2 interfaces of data type to a network declared as type "\
"'e-line' at 'network':'{}'".format(net_name)
# logger.debug(error_text)
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
elif type_ == "ptp" and net["type"] == "e-lan":
type_ = "data"
@@ -2217,7 +2263,7 @@
try:
pybindJSONDecoder.load_ietf_json(nsd_descriptor, None, None, obj=mynsd)
except Exception as e:
- raise NfvoException("Error. Invalid NS descriptor format: " + str(e), HTTP_Bad_Request)
+ raise NfvoException("Error. Invalid NS descriptor format: " + str(e), httperrors.Bad_Request)
db_scenarios = []
db_sce_nets = []
db_sce_vnfs = []
@@ -2260,7 +2306,7 @@
raise NfvoException("Error. Invalid NS descriptor at 'nsd[{}]':'constituent-vnfd':'vnfd-id-ref':"
"'{}'. Reference to a non-existing VNFD in the catalog".format(
str(nsd["id"]), str(vnf["vnfd-id-ref"])[:255]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
sce_vnf_uuid = str(uuid4())
uuid_list.append(sce_vnf_uuid)
db_sce_vnf = {
@@ -2329,7 +2375,7 @@
raise NfvoException("Error. Invalid NS descriptor at 'nsd[{}]':'vld[{}]':'ip-profile-ref':'{}'."
" Reference to a non-existing 'ip_profiles'".format(
str(nsd["id"]), str(vld["id"]), str(vld["ip-profile-ref"])),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
db_ip_profiles[ip_profile_name2db_table_index[ip_profile_name]]["sce_net_id"] = sce_net_uuid
elif vld.get("vim-network-name"):
db_sce_net["vim_network_name"] = get_str(vld, "vim-network-name", 255)
@@ -2343,7 +2389,7 @@
"-ref':'member-vnf-index-ref':'{}'. Reference to a non-existing index at "
"'nsd':'constituent-vnfd'".format(
str(nsd["id"]), str(vld["id"]), str(iface["member-vnf-index-ref"])),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
existing_ifaces = mydb.get_rows(SELECT=('i.uuid as uuid', 'i.type as iface_type'),
FROM="interfaces as i join vms on i.vm_id=vms.uuid",
@@ -2356,7 +2402,7 @@
"connection-point name at VNFD '{}'".format(
str(nsd["id"]), str(vld["id"]), str(iface["vnfd-connection-point-ref"]),
str(iface.get("vnfd-id-ref"))[:255]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
interface_uuid = existing_ifaces[0]["uuid"]
if existing_ifaces[0]["iface_type"] == "data" and not db_sce_net["type"]:
db_sce_net["type"] = "data"
@@ -2411,7 +2457,7 @@
"-ref':'member-vnf-index-ref':'{}'. Reference to a non-existing index at "
"'nsd':'constituent-vnfd'".format(
str(nsd["id"]), str(rsp["id"]), str(iface["member-vnf-index-ref"])),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
existing_ifaces = mydb.get_rows(SELECT=('i.uuid as uuid',),
FROM="interfaces as i join vms on i.vm_id=vms.uuid",
@@ -2424,7 +2470,7 @@
"connection-point name at VNFD '{}'".format(
str(nsd["id"]), str(rsp["id"]), str(iface["vnfd-connection-point-ref"]),
str(iface.get("vnfd-id-ref"))[:255]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
interface_uuid = existing_ifaces[0]["uuid"]
sce_rsp_hop_uuid = str(uuid4())
uuid_list.append(sce_rsp_hop_uuid)
@@ -2450,7 +2496,7 @@
"-ref':'member-vnf-index-ref':'{}'. Reference to a non-existing index at "
"'nsd':'constituent-vnfd'".format(
str(nsd["id"]), str(classifier["id"]), str(classifier["member-vnf-index-ref"])),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
existing_ifaces = mydb.get_rows(SELECT=('i.uuid as uuid',),
FROM="interfaces as i join vms on i.vm_id=vms.uuid",
WHERE={'vnf_id': vnf_index2vnf_uuid[vnf_index],
@@ -2462,7 +2508,7 @@
"connection-point name at VNFD '{}'".format(
str(nsd["id"]), str(rsp["id"]), str(iface["vnfd-connection-point-ref"]),
str(iface.get("vnfd-id-ref"))[:255]),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
interface_uuid = existing_ifaces[0]["uuid"]
db_sce_classifier = {
@@ -2518,7 +2564,7 @@
raise
except Exception as e:
logger.error("Exception {}".format(e))
- raise # NfvoException("Exception {}".format(e), HTTP_Bad_Request)
+ raise # NfvoException("Exception {}".format(e), httperrors.Bad_Request)
def edit_scenario(mydb, tenant_id, scenario_id, data):
@@ -2579,7 +2625,7 @@
error_text = "Error, datacenter '%s' does not have external network '%s'." % (datacenter_name, sce_net['name'])
_, message = rollback(mydb, vims, rollbackList)
logger.error("nfvo.start_scenario: %s", error_text)
- raise NfvoException(error_text, HTTP_Bad_Request)
+ raise NfvoException(error_text, httperrors.Bad_Request)
logger.debug("Using existent VIM network for scenario %s. Network id %s", scenarioDict['name'],sce_net['vim_id'])
auxNetDict['scenario'][sce_net['uuid']] = sce_net['vim_id']
@@ -2626,7 +2672,7 @@
# check if there is enough availability zones available at vim level.
if myvims[datacenter_id].availability_zone and vnf_availability_zones:
if len(vnf_availability_zones) > len(myvims[datacenter_id].availability_zone):
- raise NfvoException('No enough availability zones at VIM for this deployment', HTTP_Bad_Request)
+ raise NfvoException('No enough availability zones at VIM for this deployment', httperrors.Bad_Request)
for vm in sce_vnf['vms']:
i += 1
@@ -2682,9 +2728,9 @@
e_text = "Cannot determine the interface type PF or VF of VNF '%s' VM '%s' iface '%s'" %(sce_vnf['name'], vm['name'], iface['internal_name'])
if flavor_dict.get('extended')==None:
raise NfvoException(e_text + "After database migration some information is not available. \
- Try to delete and create the scenarios and VNFs again", HTTP_Conflict)
+ Try to delete and create the scenarios and VNFs again", httperrors.Conflict)
else:
- raise NfvoException(e_text, HTTP_Internal_Server_Error)
+ raise NfvoException(e_text, httperrors.Internal_Server_Error)
if netDict["use"]=="mgmt" or netDict["use"]=="bridge":
netDict["type"]="virtual"
if "vpci" in iface and iface["vpci"] is not None:
@@ -2861,12 +2907,12 @@
"join datacenters as d on d.uuid=dt.datacenter_id",
WHERE=where_)
if len(datacenters) > 1:
- raise NfvoException("More than one datacenters found, try to identify with uuid", HTTP_Conflict)
+ raise NfvoException("More than one datacenters found, try to identify with uuid", httperrors.Conflict)
elif datacenters:
thread_id = datacenters[0]["datacenter_tenant_id"]
thread = vim_threads["running"].get(thread_id)
if not thread:
- raise NfvoException("datacenter '{}' not found".format(str(datacenter_id_name)), HTTP_Not_Found)
+ raise NfvoException("datacenter '{}' not found".format(str(datacenter_id_name)), httperrors.Not_Found)
return thread_id, thread
except db_base_Exception as e:
raise NfvoException("{} {}".format(type(e).__name__ , str(e)), e.http_code)
@@ -2887,10 +2933,10 @@
from_ = 'datacenters as d'
vimaccounts = mydb.get_rows(FROM=from_, SELECT=("d.uuid as uuid, d.name as name",), WHERE=WHERE_dict )
if len(vimaccounts) == 0:
- raise NfvoException("datacenter '{}' not found".format(str(datacenter_id_name)), HTTP_Not_Found)
+ raise NfvoException("datacenter '{}' not found".format(str(datacenter_id_name)), httperrors.Not_Found)
elif len(vimaccounts)>1:
#print "nfvo.datacenter_action() error. Several datacenters found"
- raise NfvoException("More than one datacenters found, try to identify with uuid", HTTP_Conflict)
+ raise NfvoException("More than one datacenters found, try to identify with uuid", httperrors.Conflict)
return vimaccounts[0]["uuid"], vimaccounts[0]["name"]
@@ -2904,10 +2950,10 @@
datacenter_name = datacenter_id_name
vims = get_vim(mydb, tenant_id, datacenter_id, datacenter_name, **extra_filter)
if len(vims) == 0:
- raise NfvoException("datacenter '{}' not found".format(str(datacenter_id_name)), HTTP_Not_Found)
+ raise NfvoException("datacenter '{}' not found".format(str(datacenter_id_name)), httperrors.Not_Found)
elif len(vims)>1:
#print "nfvo.datacenter_action() error. Several datacenters found"
- raise NfvoException("More than one datacenters found, try to identify with uuid", HTTP_Conflict)
+ raise NfvoException("More than one datacenters found, try to identify with uuid", httperrors.Conflict)
return vims.keys()[0], vims.values()[0]
@@ -3018,7 +3064,7 @@
break
else:
raise NfvoException("Invalid scenario network name or id '{}' at instance:networks".format(net_name),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
if "sites" not in net_instance_desc:
net_instance_desc["sites"] = [ {} ]
site_without_datacenter_field = False
@@ -3034,7 +3080,7 @@
else:
if site_without_datacenter_field:
raise NfvoException("Found more than one entries without datacenter field at "
- "instance:networks:{}:sites".format(net_name), HTTP_Bad_Request)
+ "instance:networks:{}:sites".format(net_name), httperrors.Bad_Request)
site_without_datacenter_field = True
site["datacenter"] = default_datacenter_id # change name to id
@@ -3043,7 +3089,7 @@
if vnf_name == scenario_vnf['member_vnf_index'] or vnf_name == scenario_vnf['uuid'] or vnf_name == scenario_vnf['name']:
break
else:
- raise NfvoException("Invalid vnf name '{}' at instance:vnfs".format(vnf_name), HTTP_Bad_Request)
+ raise NfvoException("Invalid vnf name '{}' at instance:vnfs".format(vnf_name), httperrors.Bad_Request)
if "datacenter" in vnf_instance_desc:
# Add this datacenter to myvims
vnf_instance_desc["datacenter"], _ = get_datacenter_uuid(mydb, tenant_id, vnf_instance_desc["datacenter"])
@@ -3058,7 +3104,7 @@
if net_id == scenario_net['osm_id'] or net_id == scenario_net['uuid'] or net_id == scenario_net["name"]:
break
else:
- raise NfvoException("Invalid net id or name '{}' at instance:vnfs:networks".format(net_id), HTTP_Bad_Request)
+ raise NfvoException("Invalid net id or name '{}' at instance:vnfs:networks".format(net_id), httperrors.Bad_Request)
if net_instance_desc.get("vim-network-name"):
scenario_net["vim-network-name"] = net_instance_desc["vim-network-name"]
if net_instance_desc.get("name"):
@@ -3075,7 +3121,7 @@
if vdu_id == scenario_vm['osm_id'] or vdu_id == scenario_vm["name"]:
break
else:
- raise NfvoException("Invalid vdu id or name '{}' at instance:vnfs:vdus".format(vdu_id), HTTP_Bad_Request)
+ raise NfvoException("Invalid vdu id or name '{}' at instance:vnfs:vdus".format(vdu_id), httperrors.Bad_Request)
scenario_vm["instance_parameters"] = vdu_instance_desc
for iface_id, iface_instance_desc in vdu_instance_desc.get("interfaces", {}).iteritems():
for scenario_interface in scenario_vm['interfaces']:
@@ -3083,7 +3129,7 @@
scenario_interface.update(iface_instance_desc)
break
else:
- raise NfvoException("Invalid vdu id or name '{}' at instance:vnfs:vdus".format(vdu_id), HTTP_Bad_Request)
+ raise NfvoException("Invalid vdu id or name '{}' at instance:vnfs:vdus".format(vdu_id), httperrors.Bad_Request)
# 0.1 parse cloud-config parameters
cloud_config = unify_cloud_config(instance_dict.get("cloud-config"), scenarioDict.get("cloud-config"))
@@ -3198,7 +3244,7 @@
if number_mgmt_networks > 1:
raise NfvoException("Found several VLD of type mgmt. "
"You must concrete what vim-network must be use for each one",
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
create_network = False
lookfor_network = True
if vim["config"].get("management_network_id"):
@@ -3450,10 +3496,17 @@
}
task_index += 1
db_vim_actions.append(db_vim_action)
+ db_instance_action["number_tasks"] = task_index
+
+ # --> WIM
+ wan_links = wim_engine.derive_wan_links(db_instance_nets, tenant_id)
+ wim_actions = wim_engine.create_actions(wan_links)
+ wim_actions, db_instance_action = (
+ wim_engine.incorporate_actions(wim_actions, db_instance_action))
+ # <-- WIM
scenarioDict["datacenter2tenant"] = myvim_threads_id
- db_instance_action["number_tasks"] = task_index
db_instance_scenario['datacenter_tenant_id'] = myvim_threads_id[default_datacenter_id]
db_instance_scenario['datacenter_id'] = default_datacenter_id
db_tables=[
@@ -3468,7 +3521,8 @@
{"instance_sfs": db_instance_sfs},
{"instance_classifications": db_instance_classifications},
{"instance_sfps": db_instance_sfps},
- {"vim_actions": db_vim_actions}
+ {"instance_wim_nets": wan_links},
+ {"vim_wim_actions": db_vim_actions + wim_actions}
]
logger.debug("create_instance done DB tables: %s",
@@ -3477,6 +3531,8 @@
for myvim_thread_id in myvim_threads_id.values():
vim_threads["running"][myvim_thread_id].insert_task(db_vim_actions)
+ wim_engine.dispatch(wim_actions)
+
returned_instance = mydb.get_instance_scenario(instance_uuid)
returned_instance["action_id"] = instance_action_id
return returned_instance
@@ -3490,6 +3546,7 @@
error_text = "Exception"
error_text += " {} {}. {}".format(type(e).__name__, str(e), message)
# logger.error("create_instance: %s", error_text)
+ logger.exception(e)
raise NfvoException(error_text, e.http_code)
@@ -3607,7 +3664,7 @@
# check if there is enough availability zones available at vim level.
if myvims[datacenter_id].availability_zone and vnf_availability_zones:
if len(vnf_availability_zones) > len(myvims[datacenter_id].availability_zone):
- raise NfvoException('No enough availability zones at VIM for this deployment', HTTP_Bad_Request)
+ raise NfvoException('No enough availability zones at VIM for this deployment', httperrors.Bad_Request)
if sce_vnf.get("datacenter"):
vim = myvims[sce_vnf["datacenter"]]
@@ -3667,7 +3724,7 @@
extended_flavor_dict = mydb.get_rows(FROM='datacenters_flavors', SELECT=('extended',),
WHERE={'vim_id': flavor_id})
if not extended_flavor_dict:
- raise NfvoException("flavor '{}' not found".format(flavor_id), HTTP_Not_Found)
+ raise NfvoException("flavor '{}' not found".format(flavor_id), httperrors.Not_Found)
# extended_flavor_dict_yaml = yaml.load(extended_flavor_dict[0])
myVMDict['disks'] = None
@@ -3718,9 +3775,9 @@
sce_vnf['name'], vm['name'], iface['internal_name'])
if flavor_dict.get('extended') == None:
raise NfvoException(e_text + "After database migration some information is not available. \
- Try to delete and create the scenarios and VNFs again", HTTP_Conflict)
+ Try to delete and create the scenarios and VNFs again", httperrors.Conflict)
else:
- raise NfvoException(e_text, HTTP_Internal_Server_Error)
+ raise NfvoException(e_text, httperrors.Internal_Server_Error)
if netDict["use"] == "mgmt":
is_management_vm = True
netDict["type"] = "virtual"
@@ -3864,6 +3921,14 @@
instanceDict = mydb.get_instance_scenario(instance_id, tenant_id)
# print yaml.safe_dump(instanceDict, indent=4, default_flow_style=False)
tenant_id = instanceDict["tenant_id"]
+
+ # --> WIM
+ # We need to retrieve the WIM Actions now, before the instance_scenario is
+ # deleted. The reason for that is that: ON CASCADE rules will delete the
+ # instance_wim_nets record in the database
+ wim_actions = wim_engine.delete_actions(instance_scenario_id=instance_id)
+ # <-- WIM
+
# print "Checking that nfvo_tenant_id exists and getting the VIM URI and the VIM tenant_id"
# 1. Delete from Database
message = mydb.delete_instance_scenario(instance_id, tenant_id)
@@ -4136,9 +4201,15 @@
db_vim_actions.append(db_vim_action)
db_instance_action["number_tasks"] = task_index
+
+ # --> WIM
+ wim_actions, db_instance_action = (
+ wim_engine.incorporate_actions(wim_actions, db_instance_action))
+ # <-- WIM
+
db_tables = [
{"instance_actions": db_instance_action},
- {"vim_actions": db_vim_actions}
+ {"vim_wim_actions": db_vim_actions + wim_actions}
]
logger.debug("delete_instance done DB tables: %s",
@@ -4147,6 +4218,8 @@
for myvim_thread_id in vimthread_affected.keys():
vim_threads["running"][myvim_thread_id].insert_task(db_vim_actions)
+ wim_engine.dispatch(wim_actions)
+
if len(error_msg) > 0:
return 'action_id={} instance {} deleted but some elements could not be deleted, or already deleted '\
'(error: 404) from VIM: {}'.format(instance_action_id, message, error_msg)
@@ -4347,7 +4420,7 @@
#print "Checking that nfvo_tenant_id exists and getting the VIM URI and the VIM tenant_id"
vims = get_vim(mydb, nfvo_tenant, instanceDict['datacenter_id'])
if len(vims) == 0:
- raise NfvoException("datacenter '{}' not found".format(str(instanceDict['datacenter_id'])), HTTP_Not_Found)
+ raise NfvoException("datacenter '{}' not found".format(str(instanceDict['datacenter_id'])), httperrors.Not_Found)
myvim = vims.values()[0]
vm_result = {}
vm_error = 0
@@ -4381,7 +4454,7 @@
ORDER_BY="vms.created_at"
)
if not target_vms:
- raise NfvoException("Cannot find the vdu with id {}".format(vdu_id), HTTP_Not_Found)
+ raise NfvoException("Cannot find the vdu with id {}".format(vdu_id), httperrors.Not_Found)
else:
if not osm_vdu_id and not member_vnf_index:
raise NfvoException("Invalid input vdu parameters. Must supply either 'vdu-id' of 'osm_vdu_id','member-vnf-index'")
@@ -4395,7 +4468,7 @@
ORDER_BY="ivms.created_at"
)
if not target_vms:
- raise NfvoException("Cannot find the vdu with osm_vdu_id {} and member-vnf-index {}".format(osm_vdu_id, member_vnf_index), HTTP_Not_Found)
+ raise NfvoException("Cannot find the vdu with osm_vdu_id {} and member-vnf-index {}".format(osm_vdu_id, member_vnf_index), httperrors.Not_Found)
vdu_id = target_vms[-1]["uuid"]
target_vm = target_vms[-1]
datacenter = target_vm["datacenter_id"]
@@ -4436,7 +4509,7 @@
vim_action_to_clone = mydb.get_rows(FROM="vim_actions", WHERE=where)
if not vim_action_to_clone:
- raise NfvoException("Cannot find the vim_action at database with {}".format(where), HTTP_Internal_Server_Error)
+ raise NfvoException("Cannot find the vim_action at database with {}".format(where), httperrors.Internal_Server_Error)
vim_action_to_clone = vim_action_to_clone[0]
extra = yaml.safe_load(vim_action_to_clone["extra"])
@@ -4569,13 +4642,13 @@
password=password, ro_key=priv_RO_key)
else:
raise NfvoException("Unable to inject ssh key in vm: {} - Aborting".format(vm['uuid']),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
except KeyError:
raise NfvoException("Unable to inject ssh key in vm: {} - Aborting".format(vm['uuid']),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
else:
raise NfvoException("Unable to inject ssh key in vm: {} - Aborting".format(vm['uuid']),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
else:
data = myvim.action_vminstance(vm['vim_vm_id'], action_dict)
if "console" in action_dict:
@@ -4590,7 +4663,7 @@
}
vm_ok +=1
elif data["server"]=="127.0.0.1" or data["server"]=="localhost":
- vm_result[ vm['uuid'] ] = {"vim_result": -HTTP_Unauthorized,
+ vm_result[ vm['uuid'] ] = {"vim_result": -httperrors.Unauthorized,
"description": "this console is only reachable by local interface",
"name":vm['name']
}
@@ -4635,9 +4708,11 @@
rows = mydb.get_rows(FROM="instance_actions", WHERE=filter)
if action_id:
if not rows:
- raise NfvoException("Not found any action with this criteria", HTTP_Not_Found)
- vim_actions = mydb.get_rows(FROM="vim_actions", WHERE={"instance_action_id": action_id})
- rows[0]["vim_actions"] = vim_actions
+ raise NfvoException("Not found any action with this criteria", httperrors.Not_Found)
+ vim_wim_actions = mydb.get_rows(FROM="vim_wim_actions", WHERE={"instance_action_id": action_id})
+ rows[0]["vim_wim_actions"] = vim_wim_actions
+ # for backward compatibility set vim_actions = vim_wim_actions
+ rows[0]["vim_actions"] = vim_wim_actions
return {"actions": rows}
@@ -4662,15 +4737,15 @@
#port used, try with onoher
continue
except cli.ConsoleProxyException as e:
- raise NfvoException(str(e), HTTP_Bad_Request)
- raise NfvoException("Not found any free 'http_console_ports'", HTTP_Conflict)
+ raise NfvoException(str(e), httperrors.Bad_Request)
+ raise NfvoException("Not found any free 'http_console_ports'", httperrors.Conflict)
def check_tenant(mydb, tenant_id):
'''check that tenant exists at database'''
tenant = mydb.get_rows(FROM='nfvo_tenants', SELECT=('uuid',), WHERE={'uuid': tenant_id})
if not tenant:
- raise NfvoException("tenant '{}' not found".format(tenant_id), HTTP_Not_Found)
+ raise NfvoException("tenant '{}' not found".format(tenant_id), httperrors.Not_Found)
return
def new_tenant(mydb, tenant_dict):
@@ -4713,7 +4788,7 @@
# file.close(module_info[0])
raise NfvoException("Incorrect datacenter type '{}'. Plugin '{}.py' not installed".format(datacenter_type,
module),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
datacenter_id = mydb.new_row("datacenters", datacenter_descriptor, add_uuid=True, confidential_data=True)
if sdn_port_mapping:
@@ -4758,7 +4833,7 @@
for k in to_delete:
del config_dict[k]
except Exception as e:
- raise NfvoException("Bad format at datacenter:config " + str(e), HTTP_Bad_Request)
+ raise NfvoException("Bad format at datacenter:config " + str(e), httperrors.Bad_Request)
if config_dict:
datacenter_descriptor["config"] = yaml.safe_dump(config_dict, default_flow_style=True, width=256)
else:
@@ -4767,7 +4842,7 @@
try:
datacenter_sdn_port_mapping_delete(mydb, None, datacenter_id)
except ovimException as e:
- raise NfvoException("Error deleting datacenter-port-mapping " + str(e), HTTP_Conflict)
+ raise NfvoException("Error deleting datacenter-port-mapping " + str(e), httperrors.Conflict)
mydb.update_rows('datacenters', datacenter_descriptor, where)
if new_sdn_port_mapping:
@@ -4776,7 +4851,7 @@
except ovimException as e:
# Rollback
mydb.update_rows('datacenters', datacenter, where)
- raise NfvoException("Error adding datacenter-port-mapping " + str(e), HTTP_Conflict)
+ raise NfvoException("Error adding datacenter-port-mapping " + str(e), httperrors.Conflict)
return datacenter_id
@@ -4797,7 +4872,7 @@
try:
if not datacenter_id:
if not vim_id:
- raise NfvoException("You must provide 'vim_id", http_code=HTTP_Bad_Request)
+ raise NfvoException("You must provide 'vim_id", http_code=httperrors.Bad_Request)
datacenter_id = vim_id
datacenter_id, datacenter_name = get_datacenter_uuid(mydb, None, datacenter_id)
@@ -4812,7 +4887,7 @@
# #check that this association does not exist before
# tenants_datacenters = mydb.get_rows(FROM='tenants_datacenters', WHERE=tenants_datacenter_dict)
# if len(tenants_datacenters)>0:
- # raise NfvoException("datacenter '{}' and tenant'{}' are already attached".format(datacenter_id, tenant_dict['uuid']), HTTP_Conflict)
+ # raise NfvoException("datacenter '{}' and tenant'{}' are already attached".format(datacenter_id, tenant_dict['uuid']), httperrors.Conflict)
vim_tenant_id_exist_atdb=False
if not create_vim_tenant:
@@ -4838,7 +4913,7 @@
datacenter_name = myvim["name"]
vim_tenant = myvim.new_tenant(vim_tenant_name, "created by openmano for datacenter "+datacenter_name)
except vimconn.vimconnException as e:
- raise NfvoException("Not possible to create vim_tenant {} at VIM: {}".format(vim_tenant, str(e)), HTTP_Internal_Server_Error)
+ raise NfvoException("Not possible to create vim_tenant {} at VIM: {}".format(vim_tenant_id, str(e)), httperrors.Internal_Server_Error)
datacenter_tenants_dict = {}
datacenter_tenants_dict["created"]="true"
@@ -4872,7 +4947,7 @@
vim_threads["running"][thread_id] = new_thread
return thread_id
except vimconn.vimconnException as e:
- raise NfvoException(str(e), HTTP_Bad_Request)
+ raise NfvoException(str(e), httperrors.Bad_Request)
def edit_vim_account(mydb, nfvo_tenant, datacenter_tenant_id, datacenter_id=None, name=None, vim_tenant=None,
@@ -4887,9 +4962,9 @@
where_["dt.datacenter_id"] = datacenter_id
vim_accounts = mydb.get_rows(SELECT="dt.uuid as uuid, config", FROM=from_, WHERE=where_)
if not vim_accounts:
- raise NfvoException("vim_account not found for this tenant", http_code=HTTP_Not_Found)
+ raise NfvoException("vim_account not found for this tenant", http_code=httperrors.Not_Found)
elif len(vim_accounts) > 1:
- raise NfvoException("found more than one vim_account for this tenant", http_code=HTTP_Conflict)
+ raise NfvoException("found more than one vim_account for this tenant", http_code=httperrors.Conflict)
datacenter_tenant_id = vim_accounts[0]["uuid"]
original_config = vim_accounts[0]["config"]
@@ -4933,7 +5008,7 @@
tenants_datacenter_dict["nfvo_tenant_id"] = tenant_uuid
tenant_datacenter_list = mydb.get_rows(FROM='tenants_datacenters', WHERE=tenants_datacenter_dict)
if len(tenant_datacenter_list)==0 and tenant_uuid:
- raise NfvoException("datacenter '{}' and tenant '{}' are not attached".format(datacenter_id, tenant_dict['uuid']), HTTP_Not_Found)
+ raise NfvoException("datacenter '{}' and tenant '{}' are not attached".format(datacenter_id, tenant_dict['uuid']), httperrors.Not_Found)
#delete this association
mydb.delete_row(FROM='tenants_datacenters', WHERE=tenants_datacenter_dict)
@@ -4975,7 +5050,7 @@
#print content
except vimconn.vimconnException as e:
#logger.error("nfvo.datacenter_action() Not possible to get_network_list from VIM: %s ", str(e))
- raise NfvoException(str(e), HTTP_Internal_Server_Error)
+ raise NfvoException(str(e), httperrors.Internal_Server_Error)
#update nets Change from VIM format to NFVO format
net_list=[]
for net in nets:
@@ -5004,7 +5079,7 @@
return result
else:
- raise NfvoException("Unknown action " + str(action_dict), HTTP_Bad_Request)
+ raise NfvoException("Unknown action " + str(action_dict), httperrors.Bad_Request)
def datacenter_edit_netmap(mydb, tenant_id, datacenter, netmap, action_dict):
@@ -5034,11 +5109,11 @@
vim_nets = myvim.get_network_list(filter_dict=filter_dict)
except vimconn.vimconnException as e:
#logger.error("nfvo.datacenter_new_netmap() Not possible to get_network_list from VIM: %s ", str(e))
- raise NfvoException(str(e), HTTP_Internal_Server_Error)
+ raise NfvoException(str(e), httperrors.Internal_Server_Error)
if len(vim_nets)>1 and action_dict:
- raise NfvoException("more than two networks found, specify with vim_id", HTTP_Conflict)
+ raise NfvoException("more than two networks found, specify with vim_id", httperrors.Conflict)
elif len(vim_nets)==0: # and action_dict:
- raise NfvoException("Not found a network at VIM with " + str(filter_dict), HTTP_Not_Found)
+ raise NfvoException("Not found a network at VIM with " + str(filter_dict), httperrors.Not_Found)
net_list=[]
for net in vim_nets:
net_nfvo={'datacenter_id': datacenter_id}
@@ -5079,11 +5154,11 @@
# ensure the network is defined
if len(network) == 0:
raise NfvoException("Network {} is not present in the system".format(network_id),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
# ensure there is only one network with the provided name
if len(network) > 1:
- raise NfvoException("Multiple networks present in vim identified by {}".format(network_id), HTTP_Bad_Request)
+ raise NfvoException("Multiple networks present in vim identified by {}".format(network_id), httperrors.Bad_Request)
# ensure it is a dataplane network
if network[0]['type'] != 'data':
@@ -5116,7 +5191,7 @@
return sdn_net_id
else:
raise NfvoException("More than one SDN network is associated to vim network {}".format(
- network_id), HTTP_Internal_Server_Error)
+ network_id), httperrors.Internal_Server_Error)
def get_sdn_controller_id(mydb, datacenter):
# Obtain sdn controller id
@@ -5130,12 +5205,12 @@
try:
sdn_network_id = get_sdn_net_id(mydb, tenant_id, datacenter, network_id)
if not sdn_network_id:
- raise NfvoException("No SDN network is associated to vim-network {}".format(network_id), HTTP_Internal_Server_Error)
+ raise NfvoException("No SDN network is associated to vim-network {}".format(network_id), httperrors.Internal_Server_Error)
#Obtain sdn controller id
controller_id = get_sdn_controller_id(mydb, datacenter)
if not controller_id:
- raise NfvoException("No SDN controller is set for datacenter {}".format(datacenter), HTTP_Internal_Server_Error)
+ raise NfvoException("No SDN controller is set for datacenter {}".format(datacenter), httperrors.Internal_Server_Error)
#Obtain sdn controller info
sdn_controller = ovim.show_of_controller(controller_id)
@@ -5156,7 +5231,7 @@
result = ovim.new_port(port_data)
except ovimException as e:
raise NfvoException("ovimException attaching SDN network {} to vim network {}".format(
- sdn_network_id, network_id) + str(e), HTTP_Internal_Server_Error)
+ sdn_network_id, network_id) + str(e), httperrors.Internal_Server_Error)
except db_base_Exception as e:
raise NfvoException("db_base_Exception attaching SDN network to vim network {}".format(
network_id) + str(e), e.http_code)
@@ -5170,7 +5245,7 @@
sdn_network_id = get_sdn_net_id(mydb, tenant_id, datacenter, network_id)
if not sdn_network_id:
raise NfvoException("No SDN network is associated to vim-network {}".format(network_id),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
#in case no port_id is specified only ports marked as 'external_port' will be detached
filter = {'name': 'external_port', 'net_id': sdn_network_id}
@@ -5178,11 +5253,11 @@
port_list = ovim.get_ports(columns={'uuid'}, filter=filter)
except ovimException as e:
raise NfvoException("ovimException obtaining external ports for net {}. ".format(network_id) + str(e),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
if len(port_list) == 0:
raise NfvoException("No ports attached to the network {} were found with the requested criteria".format(network_id),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
port_uuid_list = []
for port in port_list:
@@ -5190,7 +5265,7 @@
port_uuid_list.append(port['uuid'])
ovim.delete_port(port['uuid'])
except ovimException as e:
- raise NfvoException("ovimException deleting port {} for net {}. ".format(port['uuid'], network_id) + str(e), HTTP_Internal_Server_Error)
+ raise NfvoException("ovimException deleting port {} for net {}. ".format(port['uuid'], network_id) + str(e), httperrors.Internal_Server_Error)
return 'Detached ports uuid: {}'.format(','.join(port_uuid_list))
@@ -5210,7 +5285,7 @@
if len(content) == 0:
raise NfvoException("Network {} is not present in the system. ".format(name),
- HTTP_Bad_Request)
+ httperrors.Bad_Request)
#Update the networks with the attached ports
for net in content:
@@ -5220,7 +5295,7 @@
#port_list = ovim.get_ports(columns={'uuid', 'switch_port', 'vlan'}, filter={'name': 'external_port', 'net_id': sdn_network_id})
port_list = ovim.get_ports(columns={'uuid', 'switch_port', 'vlan','name'}, filter={'net_id': sdn_network_id})
except ovimException as e:
- raise NfvoException("ovimException obtaining external ports for net {}. ".format(network_id) + str(e), HTTP_Internal_Server_Error)
+ raise NfvoException("ovimException obtaining external ports for net {}. ".format(network_id) + str(e), httperrors.Internal_Server_Error)
#Remove field name and if port name is external_port save it as 'type'
for port in port_list:
if port['name'] == 'external_port':
@@ -5235,7 +5310,7 @@
content = myvim.get_image_list(filter_dict=filter_dict)
else:
- raise NfvoException(item + "?", HTTP_Method_Not_Allowed)
+ raise NfvoException(item + "?", httperrors.Method_Not_Allowed)
logger.debug("vim_action response %s", content) #update nets Change from VIM format to NFVO format
if name and len(content)==1:
return {item[:-1]: content[0]}
@@ -5260,9 +5335,9 @@
logger.debug("vim_action_delete vim response: " + str(content))
items = content.values()[0]
if type(items)==list and len(items)==0:
- raise NfvoException("Not found " + item, HTTP_Not_Found)
+ raise NfvoException("Not found " + item, httperrors.Not_Found)
elif type(items)==list and len(items)>1:
- raise NfvoException("Found more than one {} with this name. Use uuid.".format(item), HTTP_Not_Found)
+ raise NfvoException("Found more than one {} with this name. Use uuid.".format(item), httperrors.Not_Found)
else: # it is a dict
item_id = items["id"]
item_name = str(items.get("name"))
@@ -5278,7 +5353,7 @@
except ovimException as e:
raise NfvoException(
"ovimException obtaining external ports for net {}. ".format(network_id) + str(e),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
# By calling one by one all ports to be detached we ensure that not only the external_ports get detached
for port in port_list:
@@ -5297,7 +5372,7 @@
except ovimException as e:
logger.error("ovimException deleting SDN network={} ".format(sdn_network_id) + str(e), exc_info=True)
raise NfvoException("ovimException deleting SDN network={} ".format(sdn_network_id) + str(e),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
content = myvim.delete_network(item_id)
elif item=="tenants":
@@ -5305,7 +5380,7 @@
elif item == "images":
content = myvim.delete_image(item_id)
else:
- raise NfvoException(item + "?", HTTP_Method_Not_Allowed)
+ raise NfvoException(item + "?", httperrors.Method_Not_Allowed)
except vimconn.vimconnException as e:
#logger.error( "vim_action Not possible to delete_{} {}from VIM: {} ".format(item, name, str(e)))
raise NfvoException("Not possible to delete_{} {} from VIM: {}".format(item, name, str(e)), e.http_code)
@@ -5346,7 +5421,7 @@
logger.error("ovimException creating SDN network={} ".format(
sdn_network) + str(e), exc_info=True)
raise NfvoException("ovimException creating SDN network={} ".format(sdn_network) + str(e),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
# Save entry in in dabase mano_db in table instance_nets to stablish a dictionary vim_net_id <->sdn_net_id
# use instance_scenario_id=None to distinguish from real instaces of nets
@@ -5364,7 +5439,7 @@
tenant = descriptor["tenant"]
content = myvim.new_tenant(tenant["name"], tenant.get("description"))
else:
- raise NfvoException(item + "?", HTTP_Method_Not_Allowed)
+ raise NfvoException(item + "?", httperrors.Method_Not_Allowed)
except vimconn.vimconnException as e:
raise NfvoException("Not possible to create {} at VIM: {}".format(item, str(e)), e.http_code)
@@ -5398,7 +5473,7 @@
if datacenter['config']:
config = yaml.load(datacenter['config'])
if 'sdn-controller' in config and config['sdn-controller'] == controller_id:
- raise NfvoException("SDN controller {} is in use by datacenter {}".format(controller_id, datacenter['uuid']), HTTP_Conflict)
+ raise NfvoException("SDN controller {} is in use by datacenter {}".format(controller_id, datacenter['uuid']), httperrors.Conflict)
data = ovim.delete_of_controller(controller_id)
msg = 'SDN controller {} deleted'.format(data)
@@ -5408,12 +5483,12 @@
def datacenter_sdn_port_mapping_set(mydb, tenant_id, datacenter_id, sdn_port_mapping):
controller = mydb.get_rows(FROM="datacenters", SELECT=("config",), WHERE={"uuid":datacenter_id})
if len(controller) < 1:
- raise NfvoException("Datacenter {} not present in the database".format(datacenter_id), HTTP_Not_Found)
+ raise NfvoException("Datacenter {} not present in the database".format(datacenter_id), httperrors.Not_Found)
try:
sdn_controller_id = yaml.load(controller[0]["config"])["sdn-controller"]
except:
- raise NfvoException("The datacenter {} has not an SDN controller associated".format(datacenter_id), HTTP_Bad_Request)
+ raise NfvoException("The datacenter {} has not an SDN controller associated".format(datacenter_id), httperrors.Bad_Request)
sdn_controller = ovim.show_of_controller(sdn_controller_id)
switch_dpid = sdn_controller["dpid"]
@@ -5429,7 +5504,7 @@
element["switch_mac"] = port.get("switch_mac")
if not pci or not (element["switch_port"] or element["switch_mac"]):
raise NfvoException ("The mapping must contain the 'pci' and at least one of the elements 'switch_port'"
- " or 'switch_mac'", HTTP_Bad_Request)
+ " or 'switch_mac'", httperrors.Bad_Request)
for pci_expanded in utils.expand_brackets(pci):
element["pci"] = pci_expanded
maps.append(dict(element))
@@ -5456,10 +5531,10 @@
result["dpid"] = sdn_controller["dpid"]
if result["sdn-controller"] == None:
- raise NfvoException("SDN controller is not defined for datacenter {}".format(datacenter_id), HTTP_Bad_Request)
+ raise NfvoException("SDN controller is not defined for datacenter {}".format(datacenter_id), httperrors.Bad_Request)
if result["dpid"] == None:
raise NfvoException("It was not possible to determine DPID for SDN controller {}".format(result["sdn-controller"]),
- HTTP_Internal_Server_Error)
+ httperrors.Internal_Server_Error)
if len(maps) == 0:
return result
@@ -5467,9 +5542,9 @@
ports_correspondence_dict = dict()
for link in maps:
if result["sdn-controller"] != link["ofc_id"]:
- raise NfvoException("The sdn-controller specified for different port mappings differ", HTTP_Internal_Server_Error)
+ raise NfvoException("The sdn-controller specified for different port mappings differ", httperrors.Internal_Server_Error)
if result["dpid"] != link["switch_dpid"]:
- raise NfvoException("The dpid specified for different port mappings differ", HTTP_Internal_Server_Error)
+ raise NfvoException("The dpid specified for different port mappings differ", httperrors.Internal_Server_Error)
element = dict()
element["pci"] = link["pci"]
if link["switch_port"]:
@@ -5508,10 +5583,10 @@
try:
public_key = key.publickey().exportKey('OpenSSH')
if isinstance(public_key, ValueError):
- raise NfvoException("Unable to create public key: {}".format(public_key), HTTP_Internal_Server_Error)
+ raise NfvoException("Unable to create public key: {}".format(public_key), httperrors.Internal_Server_Error)
private_key = key.exportKey(passphrase=tenant_id, pkcs=8)
except (ValueError, NameError) as e:
- raise NfvoException("Unable to create private key: {}".format(e), HTTP_Internal_Server_Error)
+ raise NfvoException("Unable to create private key: {}".format(e), httperrors.Internal_Server_Error)
return public_key, private_key
def decrypt_key (key, tenant_id):
@@ -5527,7 +5602,7 @@
key = RSA.importKey(key,tenant_id)
unencrypted_key = key.exportKey('PEM')
if isinstance(unencrypted_key, ValueError):
- raise NfvoException("Unable to decrypt the private key: {}".format(unencrypted_key), HTTP_Internal_Server_Error)
+ raise NfvoException("Unable to decrypt the private key: {}".format(unencrypted_key), httperrors.Internal_Server_Error)
except ValueError as e:
- raise NfvoException("Unable to decrypt the private key: {}".format(e), HTTP_Internal_Server_Error)
+ raise NfvoException("Unable to decrypt the private key: {}".format(e), httperrors.Internal_Server_Error)
return unencrypted_key
diff --git a/osm_ro/nfvo_db.py b/osm_ro/nfvo_db.py
index a40a804..9d52803 100644
--- a/osm_ro/nfvo_db.py
+++ b/osm_ro/nfvo_db.py
@@ -34,12 +34,16 @@
import time
#import sys, os
+from .http_tools import errors as httperrors
+
tables_with_createdat_field=["datacenters","instance_nets","instance_scenarios","instance_vms","instance_vnfs",
"interfaces","nets","nfvo_tenants","scenarios","sce_interfaces","sce_nets",
"sce_vnfs","tenants_datacenters","datacenter_tenants","vms","vnfs", "datacenter_nets",
- "instance_actions", "vim_actions", "sce_vnffgs", "sce_rsps", "sce_rsp_hops",
+ "instance_actions", "sce_vnffgs", "sce_rsps", "sce_rsp_hops",
"sce_classifiers", "sce_classifier_matches", "instance_sfis", "instance_sfs",
- "instance_classifications", "instance_sfps"]
+ "instance_classifications", "instance_sfps", "wims", "wim_accounts", "wim_nfvo_tenants",
+ "wim_port_mappings", "vim_wim_actions",
+ "instance_wim_nets"]
class nfvo_db(db_base.db_base):
@@ -55,7 +59,7 @@
created_time = time.time()
try:
with self.con:
-
+
myVNFDict = {}
myVNFDict["name"] = vnf_name
myVNFDict["descriptor"] = vnf_descriptor['vnf'].get('descriptor')
@@ -63,22 +67,22 @@
myVNFDict["description"] = vnf_descriptor['vnf']['description']
myVNFDict["class"] = vnf_descriptor['vnf'].get('class',"MISC")
myVNFDict["tenant_id"] = vnf_descriptor['vnf'].get("tenant_id")
-
+
vnf_id = self._new_row_internal('vnfs', myVNFDict, add_uuid=True, root_uuid=None, created_time=created_time)
#print "Adding new vms to the NFVO database"
#For each vm, we must create the appropriate vm in the NFVO database.
vmDict = {}
for _,vm in VNFCDict.iteritems():
#This code could make the name of the vms grow and grow.
- #If we agree to follow this convention, we should check with a regex that the vnfc name is not including yet the vnf name
+ #If we agree to follow this convention, we should check with a regex that the vnfc name is not including yet the vnf name
#vm['name'] = "%s-%s" % (vnf_name,vm['name'])
#print "VM name: %s. Description: %s" % (vm['name'], vm['description'])
vm["vnf_id"] = vnf_id
created_time += 0.00001
- vm_id = self._new_row_internal('vms', vm, add_uuid=True, root_uuid=vnf_id, created_time=created_time)
+ vm_id = self._new_row_internal('vms', vm, add_uuid=True, root_uuid=vnf_id, created_time=created_time)
#print "Internal vm id in NFVO DB: %s" % vm_id
vmDict[vm['name']] = vm_id
-
+
#Collect the bridge interfaces of each VM/VNFC under the 'bridge-ifaces' field
bridgeInterfacesDict = {}
for vm in vnf_descriptor['vnf']['VNFC']:
@@ -124,19 +128,19 @@
if 'internal-connections' in vnf_descriptor['vnf']:
for net in vnf_descriptor['vnf']['internal-connections']:
#print "Net name: %s. Description: %s" % (net['name'], net['description'])
-
+
myNetDict = {}
myNetDict["name"] = net['name']
myNetDict["description"] = net['description']
myNetDict["type"] = net['type']
myNetDict["vnf_id"] = vnf_id
-
+
created_time += 0.00001
net_id = self._new_row_internal('nets', myNetDict, add_uuid=True, root_uuid=vnf_id, created_time=created_time)
-
+
for element in net['elements']:
ifaceItem = {}
- #ifaceItem["internal_name"] = "%s-%s-%s" % (net['name'],element['VNFC'], element['local_iface_name'])
+ #ifaceItem["internal_name"] = "%s-%s-%s" % (net['name'],element['VNFC'], element['local_iface_name'])
ifaceItem["internal_name"] = element['local_iface_name']
#ifaceItem["vm_id"] = vmDict["%s-%s" % (vnf_name,element['VNFC'])]
ifaceItem["vm_id"] = vmDict[element['VNFC']]
@@ -159,17 +163,17 @@
created_time_iface = bridgeiface['created_time']
internalconnList.append(ifaceItem)
#print "Internal net id in NFVO DB: %s" % net_id
-
+
#print "Adding internal interfaces to the NFVO database (if any)"
for iface in internalconnList:
#print "Iface name: %s" % iface['internal_name']
iface_id = self._new_row_internal('interfaces', iface, add_uuid=True, root_uuid=vnf_id, created_time = created_time_iface)
#print "Iface id in NFVO DB: %s" % iface_id
-
+
#print "Adding external interfaces to the NFVO database"
for iface in vnf_descriptor['vnf']['external-connections']:
myIfaceDict = {}
- #myIfaceDict["internal_name"] = "%s-%s-%s" % (vnf_name,iface['VNFC'], iface['local_iface_name'])
+ #myIfaceDict["internal_name"] = "%s-%s-%s" % (vnf_name,iface['VNFC'], iface['local_iface_name'])
myIfaceDict["internal_name"] = iface['local_iface_name']
#myIfaceDict["vm_id"] = vmDict["%s-%s" % (vnf_name,iface['VNFC'])]
myIfaceDict["vm_id"] = vmDict[iface['VNFC']]
@@ -193,13 +197,13 @@
#print "Iface name: %s" % iface['name']
iface_id = self._new_row_internal('interfaces', myIfaceDict, add_uuid=True, root_uuid=vnf_id, created_time = created_time_iface)
#print "Iface id in NFVO DB: %s" % iface_id
-
+
return vnf_id
-
+
except (mdb.Error, AttributeError) as e:
self._format_error(e, tries)
tries -= 1
-
+
def new_vnf_as_a_whole2(self,nfvo_tenant,vnf_name,vnf_descriptor,VNFCDict):
self.logger.debug("Adding new vnf to the NFVO database")
tries = 2
@@ -207,7 +211,7 @@
created_time = time.time()
try:
with self.con:
-
+
myVNFDict = {}
myVNFDict["name"] = vnf_name
myVNFDict["descriptor"] = vnf_descriptor['vnf'].get('descriptor')
@@ -222,15 +226,15 @@
vmDict = {}
for _,vm in VNFCDict.iteritems():
#This code could make the name of the vms grow and grow.
- #If we agree to follow this convention, we should check with a regex that the vnfc name is not including yet the vnf name
+ #If we agree to follow this convention, we should check with a regex that the vnfc name is not including yet the vnf name
#vm['name'] = "%s-%s" % (vnf_name,vm['name'])
#print "VM name: %s. Description: %s" % (vm['name'], vm['description'])
vm["vnf_id"] = vnf_id
created_time += 0.00001
- vm_id = self._new_row_internal('vms', vm, add_uuid=True, root_uuid=vnf_id, created_time=created_time)
+ vm_id = self._new_row_internal('vms', vm, add_uuid=True, root_uuid=vnf_id, created_time=created_time)
#print "Internal vm id in NFVO DB: %s" % vm_id
vmDict[vm['name']] = vm_id
-
+
#Collect the bridge interfaces of each VM/VNFC under the 'bridge-ifaces' field
bridgeInterfacesDict = {}
for vm in vnf_descriptor['vnf']['VNFC']:
@@ -274,13 +278,13 @@
if 'internal-connections' in vnf_descriptor['vnf']:
for net in vnf_descriptor['vnf']['internal-connections']:
#print "Net name: %s. Description: %s" % (net['name'], net['description'])
-
+
myNetDict = {}
myNetDict["name"] = net['name']
myNetDict["description"] = net['description']
if (net["implementation"] == "overlay"):
net["type"] = "bridge"
- #It should give an error if the type is e-line. For the moment, we consider it as a bridge
+ #It should give an error if the type is e-line. For the moment, we consider it as a bridge
elif (net["implementation"] == "underlay"):
if (net["type"] == "e-line"):
net["type"] = "ptp"
@@ -289,10 +293,10 @@
net.pop("implementation")
myNetDict["type"] = net['type']
myNetDict["vnf_id"] = vnf_id
-
+
created_time += 0.00001
net_id = self._new_row_internal('nets', myNetDict, add_uuid=True, root_uuid=vnf_id, created_time=created_time)
-
+
if "ip-profile" in net:
ip_profile = net["ip-profile"]
myIPProfileDict = {}
@@ -305,13 +309,13 @@
myIPProfileDict["dhcp_enabled"] = ip_profile["dhcp"].get('enabled',"true")
myIPProfileDict["dhcp_start_address"] = ip_profile["dhcp"].get('start-address',None)
myIPProfileDict["dhcp_count"] = ip_profile["dhcp"].get('count',None)
-
+
created_time += 0.00001
ip_profile_id = self._new_row_internal('ip_profiles', myIPProfileDict)
-
+
for element in net['elements']:
ifaceItem = {}
- #ifaceItem["internal_name"] = "%s-%s-%s" % (net['name'],element['VNFC'], element['local_iface_name'])
+ #ifaceItem["internal_name"] = "%s-%s-%s" % (net['name'],element['VNFC'], element['local_iface_name'])
ifaceItem["internal_name"] = element['local_iface_name']
#ifaceItem["vm_id"] = vmDict["%s-%s" % (vnf_name,element['VNFC'])]
ifaceItem["vm_id"] = vmDict[element['VNFC']]
@@ -335,11 +339,11 @@
#print "Iface name: %s" % iface['internal_name']
iface_id = self._new_row_internal('interfaces', ifaceItem, add_uuid=True, root_uuid=vnf_id, created_time=created_time_iface)
#print "Iface id in NFVO DB: %s" % iface_id
-
+
#print "Adding external interfaces to the NFVO database"
for iface in vnf_descriptor['vnf']['external-connections']:
myIfaceDict = {}
- #myIfaceDict["internal_name"] = "%s-%s-%s" % (vnf_name,iface['VNFC'], iface['local_iface_name'])
+ #myIfaceDict["internal_name"] = "%s-%s-%s" % (vnf_name,iface['VNFC'], iface['local_iface_name'])
myIfaceDict["internal_name"] = iface['local_iface_name']
#myIfaceDict["vm_id"] = vmDict["%s-%s" % (vnf_name,iface['VNFC'])]
myIfaceDict["vm_id"] = vmDict[iface['VNFC']]
@@ -363,9 +367,9 @@
#print "Iface name: %s" % iface['name']
iface_id = self._new_row_internal('interfaces', myIfaceDict, add_uuid=True, root_uuid=vnf_id, created_time=created_time_iface)
#print "Iface id in NFVO DB: %s" % iface_id
-
+
return vnf_id
-
+
except (mdb.Error, AttributeError) as e:
self._format_error(e, tries)
# except KeyError as e2:
@@ -388,7 +392,7 @@
'name': scenario_dict['name'],
'description': scenario_dict['description'],
'public': scenario_dict.get('public', "false")}
-
+
scenario_uuid = self._new_row_internal('scenarios', INSERT_, add_uuid=True, root_uuid=None, created_time=created_time)
#sce_nets
for net in scenario_dict['nets'].values():
@@ -447,9 +451,9 @@
created_time += 0.00001
iface_uuid = self._new_row_internal('sce_interfaces', INSERT_, add_uuid=True,
root_uuid=scenario_uuid, created_time=created_time)
-
+
return scenario_uuid
-
+
except (mdb.Error, AttributeError) as e:
self._format_error(e, tries)
tries -= 1
@@ -465,7 +469,7 @@
#check that scenario exist
tenant_id = scenario_dict.get('tenant_id')
scenario_uuid = scenario_dict['uuid']
-
+
where_text = "uuid='{}'".format(scenario_uuid)
if not tenant_id and tenant_id != "any":
where_text += " AND (tenant_id='{}' OR public='True')".format(tenant_id)
@@ -474,9 +478,9 @@
self.cur.execute(cmd)
self.cur.fetchall()
if self.cur.rowcount==0:
- raise db_base.db_base_Exception("No scenario found with this criteria " + where_text, db_base.HTTP_Bad_Request)
+ raise db_base.db_base_Exception("No scenario found with this criteria " + where_text, httperrors.Bad_Request)
elif self.cur.rowcount>1:
- raise db_base.db_base_Exception("More than one scenario found with this criteria " + where_text, db_base.HTTP_Bad_Request)
+ raise db_base.db_base_Exception("More than one scenario found with this criteria " + where_text, httperrors.Bad_Request)
#scenario
nodes = {}
@@ -500,7 +504,7 @@
item_changed += self._update_rows('sce_nets', node, WHERE_)
item_changed += self._update_rows('sce_vnfs', node, WHERE_, modified_time=modified_time)
return item_changed
-
+
except (mdb.Error, AttributeError) as e:
self._format_error(e, tries)
tries -= 1
@@ -509,7 +513,7 @@
# '''Obtain the scenario instance information, filtering by one or serveral of the tenant, uuid or name
# instance_scenario_id is the uuid or the name if it is not a valid uuid format
# Only one scenario isntance must mutch the filtering or an error is returned
-# '''
+# '''
# print "1******************************************************************"
# try:
# with self.con:
@@ -525,11 +529,11 @@
# self.cur.execute("SELECT * FROM instance_scenarios WHERE "+ where_text)
# rows = self.cur.fetchall()
# if self.cur.rowcount==0:
-# return -HTTP_Bad_Request, "No scenario instance found with this criteria " + where_text
+# return -httperrors.Bad_Request, "No scenario instance found with this criteria " + where_text
# elif self.cur.rowcount>1:
-# return -HTTP_Bad_Request, "More than one scenario instance found with this criteria " + where_text
+# return -httperrors.Bad_Request, "More than one scenario instance found with this criteria " + where_text
# instance_scenario_dict = rows[0]
-#
+#
# #instance_vnfs
# self.cur.execute("SELECT uuid,vnf_id FROM instance_vnfs WHERE instance_scenario_id='"+ instance_scenario_dict['uuid'] + "'")
# instance_scenario_dict['instance_vnfs'] = self.cur.fetchall()
@@ -537,17 +541,17 @@
# #instance_vms
# self.cur.execute("SELECT uuid, vim_vm_id "+
# "FROM instance_vms "+
-# "WHERE instance_vnf_id='" + vnf['uuid'] +"'"
+# "WHERE instance_vnf_id='" + vnf['uuid'] +"'"
# )
# vnf['instance_vms'] = self.cur.fetchall()
# #instance_nets
# self.cur.execute("SELECT uuid, vim_net_id FROM instance_nets WHERE instance_scenario_id='"+ instance_scenario_dict['uuid'] + "'")
# instance_scenario_dict['instance_nets'] = self.cur.fetchall()
-#
+#
# #instance_interfaces
# self.cur.execute("SELECT uuid, vim_interface_id, instance_vm_id, instance_net_id FROM instance_interfaces WHERE instance_scenario_id='"+ instance_scenario_dict['uuid'] + "'")
# instance_scenario_dict['instance_interfaces'] = self.cur.fetchall()
-#
+#
# db_base._convert_datetime2str(instance_scenario_dict)
# db_base._convert_str2boolean(instance_scenario_dict, ('public','shared','external') )
# print "2******************************************************************"
@@ -561,7 +565,7 @@
scenario_id is the uuid or the name if it is not a valid uuid format
if datacenter_vim_id,d datacenter_id is provided, it supply aditional vim_id fields with the matching vim uuid
Only one scenario must mutch the filtering or an error is returned
- '''
+ '''
tries = 2
while tries:
try:
@@ -575,9 +579,9 @@
self.cur.execute(cmd)
rows = self.cur.fetchall()
if self.cur.rowcount==0:
- raise db_base.db_base_Exception("No scenario found with this criteria " + where_text, db_base.HTTP_Bad_Request)
+ raise db_base.db_base_Exception("No scenario found with this criteria " + where_text, httperrors.Bad_Request)
elif self.cur.rowcount>1:
- raise db_base.db_base_Exception("More than one scenario found with this criteria " + where_text, db_base.HTTP_Bad_Request)
+ raise db_base.db_base_Exception("More than one scenario found with this criteria " + where_text, httperrors.Bad_Request)
scenario_dict = rows[0]
if scenario_dict["cloud_config"]:
scenario_dict["cloud-config"] = yaml.load(scenario_dict["cloud_config"])
@@ -627,17 +631,17 @@
if datacenter_vim_id!=None:
cmd = "SELECT vim_id FROM datacenters_images WHERE image_id='{}' AND datacenter_vim_id='{}'".format(vm['image_id'],datacenter_vim_id)
self.logger.debug(cmd)
- self.cur.execute(cmd)
+ self.cur.execute(cmd)
if self.cur.rowcount==1:
vim_image_dict = self.cur.fetchone()
vm['vim_image_id']=vim_image_dict['vim_id']
cmd = "SELECT vim_id FROM datacenters_flavors WHERE flavor_id='{}' AND datacenter_vim_id='{}'".format(vm['flavor_id'],datacenter_vim_id)
self.logger.debug(cmd)
- self.cur.execute(cmd)
+ self.cur.execute(cmd)
if self.cur.rowcount==1:
vim_flavor_dict = self.cur.fetchone()
vm['vim_flavor_id']=vim_flavor_dict['vim_id']
-
+
#interfaces
cmd = "SELECT uuid,internal_name,external_name,net_id,type,vpci,mac,bw,model,ip_address," \
"floating_ip, port_security" \
@@ -662,15 +666,15 @@
vnf['nets'] = self.cur.fetchall()
for vnf_net in vnf['nets']:
SELECT_ = "ip_version,subnet_address,gateway_address,dns_address,dhcp_enabled,dhcp_start_address,dhcp_count"
- cmd = "SELECT {} FROM ip_profiles WHERE net_id='{}'".format(SELECT_,vnf_net['uuid'])
+ cmd = "SELECT {} FROM ip_profiles WHERE net_id='{}'".format(SELECT_,vnf_net['uuid'])
self.logger.debug(cmd)
self.cur.execute(cmd)
ipprofiles = self.cur.fetchall()
if self.cur.rowcount==1:
vnf_net["ip_profile"] = ipprofiles[0]
elif self.cur.rowcount>1:
- raise db_base.db_base_Exception("More than one ip-profile found with this criteria: net_id='{}'".format(vnf_net['uuid']), db_base.HTTP_Bad_Request)
-
+ raise db_base.db_base_Exception("More than one ip-profile found with this criteria: net_id='{}'".format(vnf_net['uuid']), httperrors.Bad_Request)
+
#sce_nets
cmd = "SELECT uuid,name,type,external,description,vim_network_name, osm_id" \
" FROM sce_nets WHERE scenario_id='{}'" \
@@ -682,28 +686,28 @@
for net in scenario_dict['nets']:
if str(net['external']) == 'false':
SELECT_ = "ip_version,subnet_address,gateway_address,dns_address,dhcp_enabled,dhcp_start_address,dhcp_count"
- cmd = "SELECT {} FROM ip_profiles WHERE sce_net_id='{}'".format(SELECT_,net['uuid'])
+ cmd = "SELECT {} FROM ip_profiles WHERE sce_net_id='{}'".format(SELECT_,net['uuid'])
self.logger.debug(cmd)
self.cur.execute(cmd)
ipprofiles = self.cur.fetchall()
if self.cur.rowcount==1:
net["ip_profile"] = ipprofiles[0]
elif self.cur.rowcount>1:
- raise db_base.db_base_Exception("More than one ip-profile found with this criteria: sce_net_id='{}'".format(net['uuid']), db_base.HTTP_Bad_Request)
+ raise db_base.db_base_Exception("More than one ip-profile found with this criteria: sce_net_id='{}'".format(net['uuid']), httperrors.Bad_Request)
continue
WHERE_=" WHERE name='{}'".format(net['name'])
if datacenter_id!=None:
WHERE_ += " AND datacenter_id='{}'".format(datacenter_id)
cmd = "SELECT vim_net_id FROM datacenter_nets" + WHERE_
self.logger.debug(cmd)
- self.cur.execute(cmd)
+ self.cur.execute(cmd)
d_net = self.cur.fetchone()
if d_net==None or datacenter_vim_id==None:
#print "nfvo_db.get_scenario() WARNING external net %s not found" % net['name']
net['vim_id']=None
else:
net['vim_id']=d_net['vim_net_id']
-
+
db_base._convert_datetime2str(scenario_dict)
db_base._convert_str2boolean(scenario_dict, ('public','shared','external','port-security','floating-ip') )
@@ -745,13 +749,13 @@
'''Deletes a scenario, filtering by one or several of the tenant, uuid or name
scenario_id is the uuid or the name if it is not a valid uuid format
Only one scenario must mutch the filtering or an error is returned
- '''
+ '''
tries = 2
while tries:
try:
with self.con:
self.cur = self.con.cursor(mdb.cursors.DictCursor)
-
+
#scenario table
where_text = "uuid='{}'".format(scenario_id)
if not tenant_id and tenant_id != "any":
@@ -761,12 +765,12 @@
self.cur.execute(cmd)
rows = self.cur.fetchall()
if self.cur.rowcount==0:
- raise db_base.db_base_Exception("No scenario found where " + where_text, db_base.HTTP_Not_Found)
+ raise db_base.db_base_Exception("No scenario found where " + where_text, httperrors.Not_Found)
elif self.cur.rowcount>1:
- raise db_base.db_base_Exception("More than one scenario found where " + where_text, db_base.HTTP_Conflict)
+ raise db_base.db_base_Exception("More than one scenario found where " + where_text, httperrors.Conflict)
scenario_uuid = rows[0]["uuid"]
scenario_name = rows[0]["name"]
-
+
#sce_vnfs
cmd = "DELETE FROM scenarios WHERE uuid='{}'".format(scenario_uuid)
self.logger.debug(cmd)
@@ -777,7 +781,7 @@
self._format_error(e, tries, "delete", "instances running")
tries -= 1
- def new_rows(self, tables, uuid_list=None):
+ def new_rows(self, tables, uuid_list=None, confidential_data=False):
"""
Make a transactional insertion of rows at several tables. Can be also a deletion
:param tables: list with dictionary where the keys are the table names and the values are a row or row list
@@ -795,6 +799,7 @@
:return: None if success, raise exception otherwise
"""
tries = 2
+ table_name = None
while tries:
created_time = time.time()
try:
@@ -819,10 +824,11 @@
else:
created_time_param = 0
self._new_row_internal(table_name, row, add_uuid=False, root_uuid=None,
- created_time=created_time_param)
+ confidential_data=confidential_data,
+ created_time=created_time_param)
return
except (mdb.Error, AttributeError) as e:
- self._format_error(e, tries)
+ self._format_error(e, tries, table=table_name)
tries -= 1
def new_instance_scenario_as_a_whole(self,tenant_id,instance_scenario_name,instance_scenario_description,scenarioDict):
@@ -845,7 +851,7 @@
INSERT_["cloud_config"] = yaml.safe_dump(scenarioDict["cloud-config"], default_flow_style=True, width=256)
instance_uuid = self._new_row_internal('instance_scenarios', INSERT_, add_uuid=True, root_uuid=None, created_time=created_time)
-
+
net_scene2instance={}
#instance_nets #nets interVNF
for net in scenarioDict['nets']:
@@ -855,10 +861,10 @@
net["vim_id_sites"] ={datacenter_site_id: net['vim_id']}
net["vim_id_sites"]["datacenter_site_id"] = {datacenter_site_id: net['vim_id']}
sce_net_id = net.get("uuid")
-
+
for datacenter_site_id,vim_id in net["vim_id_sites"].iteritems():
INSERT_={'vim_net_id': vim_id, 'created': net.get('created', False), 'instance_scenario_id':instance_uuid } #, 'type': net['type']
- INSERT_['datacenter_id'] = datacenter_site_id
+ INSERT_['datacenter_id'] = datacenter_site_id
INSERT_['datacenter_tenant_id'] = scenarioDict["datacenter2tenant"][datacenter_site_id]
if not net.get('created', False):
INSERT_['status'] = "ACTIVE"
@@ -868,31 +874,31 @@
instance_net_uuid = self._new_row_internal('instance_nets', INSERT_, True, instance_uuid, created_time)
net_scene2instance[ sce_net_id ][datacenter_site_id] = instance_net_uuid
net['uuid'] = instance_net_uuid #overwrite scnario uuid by instance uuid
-
+
if 'ip_profile' in net:
net['ip_profile']['net_id'] = None
net['ip_profile']['sce_net_id'] = None
net['ip_profile']['instance_net_id'] = instance_net_uuid
created_time += 0.00001
ip_profile_id = self._new_row_internal('ip_profiles', net['ip_profile'])
-
+
#instance_vnfs
for vnf in scenarioDict['vnfs']:
datacenter_site_id = vnf.get('datacenter_id', datacenter_id)
INSERT_={'instance_scenario_id': instance_uuid, 'vnf_id': vnf['vnf_id'] }
- INSERT_['datacenter_id'] = datacenter_site_id
+ INSERT_['datacenter_id'] = datacenter_site_id
INSERT_['datacenter_tenant_id'] = scenarioDict["datacenter2tenant"][datacenter_site_id]
if vnf.get("uuid"):
INSERT_['sce_vnf_id'] = vnf['uuid']
created_time += 0.00001
instance_vnf_uuid = self._new_row_internal('instance_vnfs', INSERT_, True, instance_uuid, created_time)
vnf['uuid'] = instance_vnf_uuid #overwrite scnario uuid by instance uuid
-
+
#instance_nets #nets intraVNF
for net in vnf['nets']:
net_scene2instance[ net['uuid'] ] = {}
INSERT_={'vim_net_id': net['vim_id'], 'created': net.get('created', False), 'instance_scenario_id':instance_uuid } #, 'type': net['type']
- INSERT_['datacenter_id'] = net.get('datacenter_id', datacenter_site_id)
+ INSERT_['datacenter_id'] = net.get('datacenter_id', datacenter_site_id)
INSERT_['datacenter_tenant_id'] = scenarioDict["datacenter2tenant"][datacenter_id]
if net.get("uuid"):
INSERT_['net_id'] = net['uuid']
@@ -900,7 +906,7 @@
instance_net_uuid = self._new_row_internal('instance_nets', INSERT_, True, instance_uuid, created_time)
net_scene2instance[ net['uuid'] ][datacenter_site_id] = instance_net_uuid
net['uuid'] = instance_net_uuid #overwrite scnario uuid by instance uuid
-
+
if 'ip_profile' in net:
net['ip_profile']['net_id'] = None
net['ip_profile']['sce_net_id'] = None
@@ -914,7 +920,7 @@
created_time += 0.00001
instance_vm_uuid = self._new_row_internal('instance_vms', INSERT_, True, instance_uuid, created_time)
vm['uuid'] = instance_vm_uuid #overwrite scnario uuid by instance uuid
-
+
#instance_interfaces
for interface in vm['interfaces']:
net_id = interface.get('net_id', None)
@@ -945,7 +951,7 @@
'''Obtain the instance information, filtering by one or several of the tenant, uuid or name
instance_id is the uuid or the name if it is not a valid uuid format
Only one instance must mutch the filtering or an error is returned
- '''
+ '''
tries = 2
while tries:
try:
@@ -969,17 +975,17 @@
self.logger.debug(cmd)
self.cur.execute(cmd)
rows = self.cur.fetchall()
-
+
if self.cur.rowcount == 0:
- raise db_base.db_base_Exception("No instance found where " + where_text, db_base.HTTP_Not_Found)
+ raise db_base.db_base_Exception("No instance found where " + where_text, httperrors.Not_Found)
elif self.cur.rowcount > 1:
raise db_base.db_base_Exception("More than one instance found where " + where_text,
- db_base.HTTP_Bad_Request)
+ httperrors.Bad_Request)
instance_dict = rows[0]
if instance_dict["cloud_config"]:
instance_dict["cloud-config"] = yaml.load(instance_dict["cloud_config"])
del instance_dict["cloud_config"]
-
+
# instance_vnfs
cmd = "SELECT iv.uuid as uuid, iv.vnf_id as vnf_id, sv.name as vnf_name, sce_vnf_id, datacenter_id"\
", datacenter_tenant_id, v.mgmt_access, sv.member_vnf_index, v.osm_id as vnfd_osm_id "\
@@ -1034,7 +1040,7 @@
del vm["vm_uuid"]
#instance_nets
- #select_text = "instance_nets.uuid as uuid,sce_nets.name as net_name,instance_nets.vim_net_id as net_id,instance_nets.status as status,instance_nets.external as external"
+ #select_text = "instance_nets.uuid as uuid,sce_nets.name as net_name,instance_nets.vim_net_id as net_id,instance_nets.status as status,instance_nets.external as external"
#from_text = "instance_nets join instance_scenarios on instance_nets.instance_scenario_id=instance_scenarios.uuid " + \
# "join sce_nets on instance_scenarios.scenario_id=sce_nets.scenario_id"
#where_text = "instance_nets.instance_scenario_id='"+ instance_dict['uuid'] + "'"
@@ -1094,18 +1100,18 @@
except (mdb.Error, AttributeError) as e:
self._format_error(e, tries)
tries -= 1
-
+
def delete_instance_scenario(self, instance_id, tenant_id=None):
'''Deletes a instance_Scenario, filtering by one or serveral of the tenant, uuid or name
instance_id is the uuid or the name if it is not a valid uuid format
Only one instance_scenario must mutch the filtering or an error is returned
- '''
+ '''
tries = 2
while tries:
try:
with self.con:
self.cur = self.con.cursor(mdb.cursors.DictCursor)
-
+
#instance table
where_list=[]
if tenant_id is not None: where_list.append( "tenant_id='" + tenant_id +"'" )
@@ -1118,24 +1124,24 @@
self.logger.debug(cmd)
self.cur.execute(cmd)
rows = self.cur.fetchall()
-
+
if self.cur.rowcount==0:
- raise db_base.db_base_Exception("No instance found where " + where_text, db_base.HTTP_Bad_Request)
+ raise db_base.db_base_Exception("No instance found where " + where_text, httperrors.Bad_Request)
elif self.cur.rowcount>1:
- raise db_base.db_base_Exception("More than one instance found where " + where_text, db_base.HTTP_Bad_Request)
+ raise db_base.db_base_Exception("More than one instance found where " + where_text, httperrors.Bad_Request)
instance_uuid = rows[0]["uuid"]
instance_name = rows[0]["name"]
-
+
#sce_vnfs
cmd = "DELETE FROM instance_scenarios WHERE uuid='{}'".format(instance_uuid)
self.logger.debug(cmd)
self.cur.execute(cmd)
-
+
return instance_uuid + " " + instance_name
except (mdb.Error, AttributeError) as e:
self._format_error(e, tries, "delete", "No dependences can avoid deleting!!!!")
tries -= 1
-
+
def new_instance_scenario(self, instance_scenario_dict, tenant_id):
#return self.new_row('vnfs', vnf_dict, None, tenant_id, True, True)
return self._new_row_internal('instance_scenarios', instance_scenario_dict, tenant_id, add_uuid=True, root_uuid=None, log=True)
@@ -1151,7 +1157,7 @@
def update_instance_vnf(self, instance_vnf_dict):
#TODO:
return
-
+
def delete_instance_vnf(self, instance_vnf_id):
#TODO:
return
@@ -1163,14 +1169,14 @@
def update_instance_vm(self, instance_vm_dict):
#TODO:
return
-
+
def delete_instance_vm(self, instance_vm_id):
#TODO:
return
def new_instance_net(self, instance_net_dict, tenant_id, instance_scenario_id = None):
return self._new_row_internal('instance_nets', instance_net_dict, tenant_id, add_uuid=True, root_uuid=instance_scenario_id, log=True)
-
+
def update_instance_net(self, instance_net_dict):
#TODO:
return
@@ -1178,7 +1184,7 @@
def delete_instance_net(self, instance_net_id):
#TODO:
return
-
+
def new_instance_interface(self, instance_interface_dict, tenant_id, instance_scenario_id = None):
return self._new_row_internal('instance_interfaces', instance_interface_dict, tenant_id, add_uuid=True, root_uuid=instance_scenario_id, log=True)
@@ -1192,7 +1198,7 @@
def update_datacenter_nets(self, datacenter_id, new_net_list=[]):
''' Removes the old and adds the new net list at datacenter list for one datacenter.
- Attribute
+ Attribute
datacenter_id: uuid of the datacenter to act upon
table: table where to insert
new_net_list: the new values to be inserted. If empty it only deletes the existing nets
@@ -1218,4 +1224,4 @@
self._format_error(e, tries)
tries -= 1
-
+
diff --git a/osm_ro/openmano_schemas.py b/osm_ro/openmano_schemas.py
index 4a7d5f5..d10f862 100644
--- a/osm_ro/openmano_schemas.py
+++ b/osm_ro/openmano_schemas.py
@@ -22,7 +22,7 @@
##
'''
-JSON schemas used by openmano httpserver.py module to parse the different files and messages sent through the API
+JSON schemas used by openmano httpserver.py module to parse the different files and messages sent through the API
'''
__author__="Alfonso Tierno, Gerardo Garcia, Pablo Montes"
__date__ ="$09-oct-2014 09:09:48$"
@@ -48,7 +48,7 @@
path_schema={"type":"string", "pattern":"^(\.){0,2}(/[^/\"':{}\(\)]+)+$"}
vlan_schema={"type":"integer","minimum":1,"maximum":4095}
vlan1000_schema={"type":"integer","minimum":1000,"maximum":4095}
-mac_schema={"type":"string", "pattern":"^[0-9a-fA-F][02468aceACE](:[0-9a-fA-F]{2}){5}$"} #must be unicast LSB bit of MSB byte ==0
+mac_schema={"type":"string", "pattern":"^[0-9a-fA-F][02468aceACE](:[0-9a-fA-F]{2}){5}$"} #must be unicast LSB bit of MSB byte ==0
#mac_schema={"type":"string", "pattern":"^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$"}
ip_schema={"type":"string","pattern":"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"}
ip_prefix_schema={"type":"string","pattern":"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/(30|[12]?[0-9])$"}
@@ -99,13 +99,13 @@
"vim_name": nameshort_schema,
"vim_tenant_name": nameshort_schema,
"mano_tenant_name": nameshort_schema,
- "mano_tenant_id": id_schema,
+ "mano_tenant_id": id_schema,
"http_console_proxy": {"type":"boolean"},
"http_console_host": nameshort_schema,
"http_console_ports": {
- "type": "array",
+ "type": "array",
"items": {"OneOf": [
- port_schema,
+ port_schema,
{"type": "object", "properties": {"from": port_schema, "to": port_schema}, "required": ["from", "to"]}
]}
},
@@ -113,12 +113,14 @@
"log_socket_level": log_level_schema,
"log_level_db": log_level_schema,
"log_level_vim": log_level_schema,
+ "log_level_wim": log_level_schema,
"log_level_nfvo": log_level_schema,
"log_level_http": log_level_schema,
"log_level_console": log_level_schema,
"log_level_ovim": log_level_schema,
"log_file_db": path_schema,
"log_file_vim": path_schema,
+ "log_file_wim": path_schema,
"log_file_nfvo": path_schema,
"log_file_http": path_schema,
"log_file_console": path_schema,
@@ -430,7 +432,7 @@
"properties":{
"name": name_schema,
"mgmt": {"type":"boolean"},
- "type": {"type": "string", "enum":["e-line", "e-lan"]},
+ "type": {"type": "string", "enum":["e-line", "e-lan"]},
"implementation": {"type": "string", "enum":["overlay", "underlay"]},
"VNFC": name_schema,
"local_iface_name": name_schema ,
@@ -580,7 +582,7 @@
"bridge-ifaces": bridge_interfaces_schema,
"devices": devices_schema,
"boot-data" : boot_data_vdu_schema
-
+
},
"required": ["name"],
"oneOf": [
@@ -767,7 +769,7 @@
},
}
},
-
+
},
"required": ["vnfs", "name"],
"additionalProperties": False
@@ -861,7 +863,7 @@
},
}
},
-
+
},
"required": ["vnfs", "networks","name"],
"additionalProperties": False
@@ -1056,7 +1058,7 @@
"vim-network-name": name_schema,
"ip-profile": ip_profile_schema,
"name": name_schema,
- }
+ }
}
}
},
@@ -1086,7 +1088,7 @@
}
},
"ip-profile": ip_profile_schema,
- #if the network connects VNFs deployed at different sites, you must specify one entry per site that this network connect to
+ #if the network connects VNFs deployed at different sites, you must specify one entry per site that this network connect to
"sites": {
"type":"array",
"minLength":1,
@@ -1095,16 +1097,16 @@
"properties":{
# By default for an scenario 'external' network openmano looks for an existing VIM network to map this external scenario network,
# for other networks openamno creates at VIM
- # Use netmap-create to force to create an external scenario network
+ # Use netmap-create to force to create an external scenario network
"netmap-create": {"oneOf":[name_schema,{"type": "null"}]}, #datacenter network to use. Null if must be created as an internal net
- #netmap-use: Indicates an existing VIM network that must be used for this scenario network.
+ #netmap-use: Indicates an existing VIM network that must be used for this scenario network.
#Can use both the VIM network name (if it is not ambiguous) or the VIM net UUID
#If both 'netmap-create' and 'netmap-use'are supplied, netmap-use precedes, but if fails openmano follows the netmap-create
#In oder words, it is the same as 'try to map to the VIM network (netmap-use) if exist, and if not create the network (netmap-create)
- "netmap-use": name_schema, #
+ "netmap-use": name_schema, #
"vim-network-name": name_schema, #override network name
#"ip-profile": ip_profile_schema,
- "datacenter": name_schema,
+ "datacenter": name_schema,
}
}
},
diff --git a/osm_ro/openmanoclient.py b/osm_ro/openmanoclient.py
index 897eb9a..e15824a 100644
--- a/osm_ro/openmanoclient.py
+++ b/osm_ro/openmanoclient.py
@@ -565,7 +565,168 @@
if mano_response.status_code==200:
return content
else:
- raise OpenmanoResponseException(str(content))
+ raise OpenmanoResponseException(str(content))
+
+ # WIMS
+
+ def list_wims(self, all_tenants=False, **kwargs):
+ '''Obtain a list of wims, that are the WIM information at openmano
+ Params: can be filtered by 'uuid','name','wim_url','type'
+ Return: Raises an exception on error
+ Obtain a dictionary with format {'wims':[{wim1_info},{wim2_info},...]}}
+ '''
+ return self._list_item("wims", all_tenants, filter_dict=kwargs)
+
+ def get_wim(self, uuid=None, name=None, all_tenants=False):
+ '''Obtain the information of a wim
+ Params: uuid or/and name. If only name is supplied, there must be only one or an exception is raised
+ Return: Raises an exception on error, not found, found several
+ Obtain a dictionary with format {'wim':{wim_info}}
+ '''
+ return self._get_item("wims", uuid, name, all_tenants)
+
+ def delete_wim(self, uuid=None, name=None):
+ '''Delete a wim
+ Params: uuid or/and name. If only name is supplied, there must be only one or an exception is raised
+ Return: Raises an exception on error, not found, found several, not free
+ Obtain a dictionary with format {'result': text indicating deleted}
+ '''
+ if not uuid:
+ # check that exist
+ uuid = self._get_item_uuid("wims", uuid, name, all_tenants=True)
+ return self._del_item("wims", uuid, name, all_tenants=None)
+
+ def create_wim(self, descriptor=None, descriptor_format=None, name=None, wim_url=None, **kwargs):
+ # , type="openvim", public=False, description=None):
+ '''Creates a wim
+ Params: must supply a descriptor or/and just a name and a wim_url
+ descriptor: with format {'wim':{new_wim_info}}
+ new_wim_info must contain 'name', 'wim_url', and optionally 'description'
+ must be a dictionary or a json/yaml text.
+ name: the wim name. Overwrite descriptor name if any
+ wim_url: the wim URL. Overwrite descriptor vim_url if any
+ wim_type: the WIM type, can be tapi, odl, onos. Overwrite descriptor type if any
+ public: boolean, by default not public
+ description: wim description. Overwrite descriptor description if any
+ config: dictionary with extra configuration for the concrete wim
+ Return: Raises an exception on error
+ Obtain a dictionary with format {'wim:{new_wim_info}}
+ '''
+ if isinstance(descriptor, str):
+ descriptor = self.parse(descriptor, descriptor_format)
+ elif descriptor:
+ pass
+ elif name and wim_url:
+ descriptor = {"wim": {"name": name, "wim_url": wim_url}}
+ else:
+ raise OpenmanoBadParamsException("Missing descriptor, or name and wim_url")
+
+ if 'wim' not in descriptor or len(descriptor) != 1:
+ raise OpenmanoBadParamsException("Descriptor must contain only one 'wim' field")
+ if name:
+ descriptor['wim']['name'] = name
+ if wim_url:
+ descriptor['wim']['wim_url'] = wim_url
+ for param in kwargs:
+ descriptor['wim'][param] = kwargs[param]
+
+ return self._create_item("wims", descriptor, all_tenants=None)
+
+ def edit_wim(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False,
+ **kwargs):
+ '''Edit the parameters of a wim
+ Params: must supply a descriptor or/and a parameter to change
+ uuid or/and name. If only name is supplied, there must be only one or an exception is raised
+ descriptor: with format {'wim':{params to change info}}
+ must be a dictionary or a json/yaml text.
+ parameters to change can be supplied by the descriptor or as parameters:
+ new_name: the wim name
+ wim_url: the wim URL
+ wim_type: the wim type, can be tapi, onos, odl
+ public: boolean, available to other tenants
+ description: wim description
+ Return: Raises an exception on error, not found or found several
+ Obtain a dictionary with format {'wim':{new_wim_info}}
+ '''
+ if isinstance(descriptor, str):
+ descriptor = self.parse(descriptor, descriptor_format)
+ elif descriptor:
+ pass
+ elif kwargs:
+ descriptor = {"wim": {}}
+ else:
+ raise OpenmanoBadParamsException("Missing descriptor")
+
+ if 'wim' not in descriptor or len(descriptor) != 1:
+ raise OpenmanoBadParamsException("Descriptor must contain only one 'wim' field")
+ for param in kwargs:
+ if param == 'new_name':
+ descriptor['wim']['name'] = kwargs[param]
+ else:
+ descriptor['wim'][param] = kwargs[param]
+ return self._edit_item("wims", descriptor, uuid, name, all_tenants=None)
+
+ def attach_wim(self, uuid=None, name=None, descriptor=None, descriptor_format=None, wim_user=None,
+ wim_password=None, wim_tenant_name=None, wim_tenant_id=None):
+ # check that exist
+ uuid = self._get_item_uuid("wims", uuid, name, all_tenants=True)
+ tenant_text = "/" + self._get_tenant()
+
+ if isinstance(descriptor, str):
+ descriptor = self.parse(descriptor, descriptor_format)
+ elif descriptor:
+ pass
+ elif wim_user or wim_password or wim_tenant_name or wim_tenant_id:
+ descriptor = {"wim": {}}
+ else:
+ raise OpenmanoBadParamsException("Missing descriptor or params")
+
+ if wim_user or wim_password or wim_tenant_name or wim_tenant_id:
+ # print args.name
+ try:
+ if wim_user:
+ descriptor['wim']['wim_user'] = wim_user
+ if wim_password:
+ descriptor['wim']['wim_password'] = wim_password
+ if wim_tenant_name:
+ descriptor['wim']['wim_tenant_name'] = wim_tenant_name
+ if wim_tenant_id:
+ descriptor['wim']['wim_tenant'] = wim_tenant_id
+ except (KeyError, TypeError) as e:
+ if str(e) == 'wim':
+ error_pos = "missing field 'wim'"
+ else:
+ error_pos = "wrong format"
+ raise OpenmanoBadParamsException("Wrong wim descriptor: " + error_pos)
+
+ payload_req = yaml.safe_dump(descriptor)
+ # print payload_req
+ URLrequest = "{}{}/wims/{}".format(self.endpoint_url, tenant_text, uuid)
+ self.logger.debug("openmano POST %s %s", URLrequest, payload_req)
+ mano_response = requests.post(URLrequest, headers=self.headers_req, data=payload_req)
+ self.logger.debug("openmano response: %s", mano_response.text)
+
+ content = self._parse_yaml(mano_response.text, response=True)
+ if mano_response.status_code == 200:
+ return content
+ else:
+ raise OpenmanoResponseException(str(content))
+
+ def detach_wim(self, uuid=None, name=None):
+ if not uuid:
+ # check that exist
+ uuid = self._get_item_uuid("wims", uuid, name, all_tenants=False)
+ tenant_text = "/" + self._get_tenant()
+ URLrequest = "{}{}/wims/{}".format(self.endpoint_url, tenant_text, uuid)
+ self.logger.debug("openmano DELETE %s", URLrequest)
+ mano_response = requests.delete(URLrequest, headers=self.headers_req)
+ self.logger.debug("openmano response: %s", mano_response.text)
+
+ content = self._parse_yaml(mano_response.text, response=True)
+ if mano_response.status_code == 200:
+ return content
+ else:
+ raise OpenmanoResponseException(str(content))
#VNFS
def list_vnfs(self, all_tenants=False, **kwargs):
diff --git a/osm_ro/openmanod.cfg b/osm_ro/openmanod.cfg
index eeefcb8..3565bbf 100644
--- a/osm_ro/openmanod.cfg
+++ b/osm_ro/openmanod.cfg
@@ -71,6 +71,8 @@
#log_file_db: /opt/openmano/logs/openmano_db.log
#log_level_vim: DEBUG #VIM connection log levels
#log_file_vim: /opt/openmano/logs/openmano_vimconn.log
+#log_level_wim: DEBUG #WIM connection log levels
+#log_file_wim: /opt/openmano/logs/openmano_wimconn.log
#log_level_nfvo: DEBUG #Main engine log levels
#log_file_nfvo: /opt/openmano/logs/openmano_nfvo.log
#log_level_http: DEBUG #Main engine log levels
diff --git a/osm_ro/tests/db_helpers.py b/osm_ro/tests/db_helpers.py
new file mode 100644
index 0000000..bedf9a5
--- /dev/null
+++ b/osm_ro/tests/db_helpers.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+import hashlib
+import shlex
+import unittest
+from contextlib import contextmanager
+from functools import wraps
+from hashlib import md5
+from os import environ, pathsep
+from subprocess import STDOUT, check_output
+from uuid import UUID
+
+from MySQLdb import connect
+
+from ..nfvo_db import nfvo_db
+
+HOST = environ.get('TEST_DB_HOST', 'localhost')
+USER = environ.get('TEST_DB_USER', 'mano')
+PASSWORD = environ.get('TEST_DB_PASSWORD', 'manopw')
+DATABASE = environ.get('TEST_DB_DATABASE', 'mano_db')
+
+
+def uuid(seed):
+ """Generates strings with a UUID format in a repeatable way"""
+ return str(UUID(md5(str(seed)).hexdigest()))
+
+
+def sha1(text):
+ """Generates SHA1 hash code from a text string"""
+ return hashlib.sha1(text).hexdigest()
+
+
+def run(*args, **kwargs):
+ """Run a command inside a subprocess, raising an exception when it fails
+
+ Arguments:
+ *args: you can pass any number of arquments as separated words in the
+ shell, or just a single string with the entire command
+ **kwargs: proxied to subprocess.check_output (by default
+ ``stderr=STDOUT`` and ``universal_newlines=True``
+ """
+ if len(args) == 1 and isinstance(args[0], str):
+ args = shlex.split(args[0])
+
+ opts = dict(stderr=STDOUT, universal_newlines=True)
+ opts.update(kwargs)
+ return check_output(args, **opts)
+
+
+# In order to not mess around, enforce user to explicit set the
+# test database in a env variable
+@unittest.skipUnless(
+ environ.get('TEST_DB_HOST'),
+ 'Test database not available. Please set TEST_DB_HOST env var')
+class TestCaseWithDatabase(unittest.TestCase):
+ """Connect to the database and provide methods to facilitate isolating the
+ database stored inside it between tests.
+
+ In order to avoid connecting, reconnecting, creating tables and destroying
+ tables all the time, this class manage the database using class-level
+ fixtures. This reduce the cost of performing these actions but not
+ guarantees isolation in the DB state between the tests.
+ To enforce isolation, please call the ``setup_tables`` and
+ ``empty_database`` directly, or write one single test per class.
+ """
+
+ host = HOST
+ user = USER
+ password = PASSWORD
+ database = DATABASE
+
+ @classmethod
+ def setup_tables(cls):
+ """Make sure the database is set up and in the right version, with all the
+ required tables.
+ """
+ dbutils = environ.get('DBUTILS')
+
+ if dbutils:
+ environ["PATH"] += pathsep + dbutils
+
+ return run('init_mano_db.sh',
+ '-u', cls.user,
+ '-p', cls.password,
+ '-h', cls.host,
+ '-d', cls.database)
+
+ @classmethod
+ def empty_database(cls):
+ """Clear the database, so one test does not interfere with the other"""
+ # Create a custom connection not attached to the database, so we can
+ # destroy and recreate the database itself
+ connection = connect(cls.host, cls.user, cls.password)
+ cursor = connection.cursor()
+ cursor.execute(
+ "DROP DATABASE {};".format(
+ connection.escape_string(cls.database)))
+ cursor.execute(
+ "CREATE DATABASE {};".format(
+ connection.escape_string(cls.database)))
+ cursor.close()
+ connection.close()
+
+
+class TestCaseWithDatabasePerTest(TestCaseWithDatabase):
+ """Ensure a connection to the database before and
+ drop tables after each test runs
+ """
+
+ def setUp(self):
+ self.setup_tables()
+ self.addCleanup(self.empty_database)
+
+ self.maxDiff = None
+
+ self.db = nfvo_db(self.host, self.user, self.password, self.database)
+ self.db.connect()
+
+ def populate(self, seeds=None, **kwargs):
+ """Seed the database with initial values"""
+ if not seeds:
+ seeds = []
+ if not isinstance(seeds, (list, tuple)):
+ seeds = [seeds]
+ if kwargs:
+ seeds.append(kwargs)
+ self.db.new_rows(seeds)
+
+ def count(self, table):
+ """Count number of rows in a table"""
+ return self.db.get_rows(
+ SELECT='COUNT(*) as count', FROM=table)[0]['count']
+
+ @contextmanager
+ def disable_foreign_keys(self):
+ """Do the test without checking foreign keys"""
+ try:
+ cursor = self.db.con.cursor()
+ cursor.execute('SET FOREIGN_KEY_CHECKS=0;')
+ yield
+ finally:
+ cursor.execute('SET FOREIGN_KEY_CHECKS=1;')
+
+
+def disable_foreign_keys(test):
+ """Do the test without checking foreign keys.
+ To be used together in subclasses of TestCaseWithDatabasePerTest
+ """
+ @wraps(test)
+ def _no_check(self, *args, **kwargs):
+ with self.disable_foreign_keys():
+ result = test(self, *args, **kwargs)
+
+ return result
+
+ return _no_check
diff --git a/osm_ro/tests/helpers.py b/osm_ro/tests/helpers.py
new file mode 100644
index 0000000..787fbce
--- /dev/null
+++ b/osm_ro/tests/helpers.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+import logging
+import unittest
+from collections import defaultdict
+
+from six import StringIO
+
+from mock import MagicMock, patch
+
+logger = logging.getLogger()
+
+
+class TestCaseWithLogging(unittest.TestCase):
+ """Attach a special handler to the root logger, capturing the logs in a
+ internal buffer (caplog property).
+
+ To retrieve the logs, do::
+
+ self.caplog.getvalue()
+ """
+ def setUp(self):
+ super(TestCaseWithLogging, self).setUp()
+ self.logger = logging.getLogger()
+ self.caplog = StringIO()
+ self.log_handler = logging.StreamHandler(self.caplog)
+ self.logger.addHandler(self.log_handler)
+ self.logger.setLevel(logging.NOTSET)
+
+ def tearDown(self):
+ super(TestCaseWithLogging, self).tearDown()
+ self.log_handler.close()
+ self.logger.removeHandler(self.log_handler)
+
+
+def mock_imports(modules, preserve=()):
+ """Given a list of modules, mock everything, unless listed in the preserve
+ argument.
+ """
+ # Ensure iterable
+ if isinstance(modules, str):
+ modules = (modules,)
+ if isinstance(preserve, str):
+ preserve = (preserve,)
+
+ # First expand the list, since children modules needs their parent also
+ # mocked most of the time.
+ # Example: ['Crypto.PublicKey'] => ['Crypto', 'Crypto.PublicKey']
+ all_modules = []
+ for name in modules:
+ parts = name.split('.')
+ compound_name = []
+ for part in parts:
+ compound_name.append(part)
+ all_modules.append('.'.join(compound_name))
+
+ all_modules = set(m for m in all_modules if m not in preserve)
+ for module in all_modules:
+ logger.info('Mocking module `%s`', module)
+
+ mocks = {module: MagicMock() for module in all_modules}
+
+ return patch.dict('sys.modules', **mocks)
+
+
+def mock_dict(**kwargs):
+ """Create a dict that always respond something.
+
+ Arguments:
+ **kwargs: certain items that should be set in the created object
+ """
+ response = defaultdict(MagicMock)
+ for k, v in kwargs.items():
+ response[k] = v
+
+ return response
+
+
+def mock_object(**kwargs):
+ """Create an object that always respond something.
+
+ Arguments:
+ **kwargs: certain attributes that should be set in the created object
+ """
+ response = MagicMock()
+ for k, v in kwargs.items():
+ setattr(response, k, v)
+
+ return response
diff --git a/osm_ro/utils.py b/osm_ro/utils.py
index 0ee8efc..2afbc85 100644
--- a/osm_ro/utils.py
+++ b/osm_ro/utils.py
@@ -30,8 +30,16 @@
__date__ ="$08-sep-2014 12:21:22$"
import datetime
+import time
import warnings
-from jsonschema import validate as js_v, exceptions as js_e
+from functools import reduce
+from itertools import tee
+
+from six.moves import filter, filterfalse
+
+from jsonschema import exceptions as js_e
+from jsonschema import validate as js_v
+
#from bs4 import BeautifulSoup
def read_file(file_to_read):
@@ -42,7 +50,7 @@
f.close()
except Exception as e:
return (False, str(e))
-
+
return (True, read_data)
def write_file(file_to_write, text):
@@ -53,7 +61,7 @@
f.close()
except Exception as e:
return (False, str(e))
-
+
return (True, None)
def format_in(http_response, schema):
@@ -93,8 +101,22 @@
# return text
+def delete_nulls(var):
+ if type(var) is dict:
+ for k in var.keys():
+ if var[k] is None: del var[k]
+ elif type(var[k]) is dict or type(var[k]) is list or type(var[k]) is tuple:
+ if delete_nulls(var[k]): del var[k]
+ if len(var) == 0: return True
+ elif type(var) is list or type(var) is tuple:
+ for k in var:
+ if type(k) is dict: delete_nulls(k)
+ if len(var) == 0: return True
+ return False
+
+
def convert_bandwidth(data, reverse=False):
- '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
+ '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
It assumes that bandwidth is well formed
Attributes:
'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid
@@ -127,7 +149,21 @@
if type(k) is dict or type(k) is tuple or type(k) is list:
convert_bandwidth(k, reverse)
-
+def convert_float_timestamp2str(var):
+ '''Converts timestamps (created_at, modified_at fields) represented as float
+ to a string with the format '%Y-%m-%dT%H:%i:%s'
+ It enters recursively in the dict var finding this kind of variables
+ '''
+ if type(var) is dict:
+ for k,v in var.items():
+ if type(v) is float and k in ("created_at", "modified_at"):
+ var[k] = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(v) )
+ elif type(v) is dict or type(v) is list or type(v) is tuple:
+ convert_float_timestamp2str(v)
+ if len(var) == 0: return True
+ elif type(var) is list or type(var) is tuple:
+ for v in var:
+ convert_float_timestamp2str(v)
def convert_datetime2str(var):
'''Converts a datetime variable to a string with the format '%Y-%m-%dT%H:%i:%s'
@@ -137,7 +173,7 @@
for k,v in var.items():
if type(v) is datetime.datetime:
var[k]= v.strftime('%Y-%m-%dT%H:%M:%S')
- elif type(v) is dict or type(v) is list or type(v) is tuple:
+ elif type(v) is dict or type(v) is list or type(v) is tuple:
convert_datetime2str(v)
if len(var) == 0: return True
elif type(var) is list or type(var) is tuple:
@@ -145,7 +181,7 @@
convert_datetime2str(v)
def convert_str2boolean(data, items):
- '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
+ '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
Done recursively
Attributes:
'data': dictionary variable to be checked. None or empty is considered valid
@@ -207,4 +243,105 @@
warnings.simplefilter('default', DeprecationWarning)
return func(*args, **kwargs)
return deprecated_func
- return deprecated_decorator
\ No newline at end of file
+ return deprecated_decorator
+
+
+def truncate(text, max_length=1024):
+ """Limit huge texts in number of characters"""
+ text = str(text)
+ if text and len(text) >= max_length:
+ return text[:max_length//2-3] + " ... " + text[-max_length//2+3:]
+ return text
+
+
+def merge_dicts(*dicts, **kwargs):
+ """Creates a new dict merging N others and keyword arguments.
+ Right-most dicts take precedence.
+ Keyword args take precedence.
+ """
+ return reduce(
+ lambda acc, x: acc.update(x) or acc,
+ list(dicts) + [kwargs], {})
+
+
+def remove_none_items(adict):
+ """Return a similar dict without keys associated to None values"""
+ return {k: v for k, v in adict.items() if v is not None}
+
+
+def filter_dict_keys(adict, allow):
+ """Return a similar dict, but just containing the explicitly allowed keys
+
+ Arguments:
+ adict (dict): Simple python dict data struct
+ allow (list): Explicits allowed keys
+ """
+ return {k: v for k, v in adict.items() if k in allow}
+
+
+def filter_out_dict_keys(adict, deny):
+ """Return a similar dict, but not containing the explicitly denied keys
+
+ Arguments:
+ adict (dict): Simple python dict data struct
+ deny (list): Explicits denied keys
+ """
+ return {k: v for k, v in adict.items() if k not in deny}
+
+
+def expand_joined_fields(record):
+ """Given a db query result, explode the fields that contains `.` (join
+ operations).
+
+ Example
+ >> expand_joined_fiels({'wim.id': 2})
+ # {'wim': {'id': 2}}
+ """
+ result = {}
+ for field, value in record.items():
+ keys = field.split('.')
+ target = result
+ target = reduce(lambda target, key: target.setdefault(key, {}),
+ keys[:-1], result)
+ target[keys[-1]] = value
+
+ return result
+
+
+def ensure(condition, exception):
+ """Raise an exception if condition is not met"""
+ if not condition:
+ raise exception
+
+
+def partition(predicate, iterable):
+ """Create two derived iterators from a single one
+ The first iterator created will loop thought the values where the function
+ predicate is True, the second one will iterate over the values where it is
+ false.
+ """
+ iterable1, iterable2 = tee(iterable)
+ return filter(predicate, iterable2), filterfalse(predicate, iterable1)
+
+
+def pipe(*functions):
+ """Compose functions of one argument in the opposite order,
+ So pipe(f, g)(x) = g(f(x))
+ """
+ return lambda x: reduce(lambda acc, f: f(acc), functions, x)
+
+
+def compose(*functions):
+ """Compose functions of one argument,
+ So compose(f, g)(x) = f(g(x))
+ """
+ return lambda x: reduce(lambda acc, f: f(acc), functions[::-1], x)
+
+
+def safe_get(target, key_path, default=None):
+ """Given a path of keys (eg.: "key1.key2.key3"), return a nested value in
+ a nested dict if present, or the default value
+ """
+ keys = key_path.split('.')
+ target = reduce(lambda acc, key: acc.get(key) or {}, keys[:-1], target)
+ return target.get(keys[-1], default)
diff --git a/osm_ro/vim_thread.py b/osm_ro/vim_thread.py
index b70d94a..48c8e32 100644
--- a/osm_ro/vim_thread.py
+++ b/osm_ro/vim_thread.py
@@ -23,7 +23,7 @@
""""
This is thread that interacts with a VIM. It processes TASKs sequentially against a single VIM.
-The tasks are stored at database in table vim_actions
+The tasks are stored at database in table vim_wim_actions
The task content is (M: stored at memory, D: stored at database):
MD instance_action_id: reference a global action over an instance-scenario: database instance_actions
MD task_index: index number of the task. This together with the previous forms a unique key identifier
@@ -49,8 +49,8 @@
vim_status: VIM status of the element. Stored also at database in the instance_XXX
M depends: dict with task_index(from depends_on) to task class
M params: same as extra[params] but with the resolved dependencies
- M vim_interfaces: similar to extra[interfaces] but with VIM information. Stored at database in the instance_XXX but not at vim_actions
- M vim_info: Detailed information of a vm,net from the VIM. Stored at database in the instance_XXX but not at vim_actions
+ M vim_interfaces: similar to extra[interfaces] but with VIM information. Stored at database in the instance_XXX but not at vim_wim_actions
+ M vim_info: Detailed information of a vm,net from the VIM. Stored at database in the instance_XXX but not at vim_wim_actions
MD error_msg: descriptive text upon an error.Stored also at database instance_XXX
MD created_at: task creation time
MD modified_at: last task update time. On refresh it contains when this task need to be refreshed
@@ -189,7 +189,7 @@
while True:
# get 200 (database_limit) entries each time
with self.db_lock:
- vim_actions = self.db.get_rows(FROM="vim_actions",
+ vim_actions = self.db.get_rows(FROM="vim_wim_actions",
WHERE={"datacenter_vim_id": self.datacenter_tenant_id,
"item_id>=": old_item_id},
ORDER_BY=("item_id", "item", "created_at",),
@@ -393,7 +393,7 @@
if task_need_update:
with self.db_lock:
self.db.update_rows(
- 'vim_actions',
+ 'vim_wim_actions',
UPDATE={"extra": yaml.safe_dump(task["extra"], default_flow_style=True, width=256),
"error_msg": task.get("error_msg"), "modified_at": now},
WHERE={'instance_action_id': task['instance_action_id'],
@@ -463,7 +463,7 @@
with self.db_lock:
self.db.update_rows('instance_nets', UPDATE=temp_dict, WHERE={"uuid": task["item_id"]})
self.db.update_rows(
- 'vim_actions',
+ 'vim_wim_actions',
UPDATE={"extra": yaml.safe_dump(task["extra"], default_flow_style=True, width=256),
"error_msg": task.get("error_msg"), "modified_at": now},
WHERE={'instance_action_id': task['instance_action_id'],
@@ -644,7 +644,7 @@
now = time.time()
with self.db_lock:
self.db.update_rows(
- table="vim_actions",
+ table="vim_wim_actions",
UPDATE={"status": task["status"], "vim_id": task.get("vim_id"), "modified_at": now,
"error_msg": task["error_msg"],
"extra": yaml.safe_dump(task["extra"], default_flow_style=True, width=256)},
@@ -811,7 +811,7 @@
instance_action_id = ins_action_id
with self.db_lock:
- tasks = self.db.get_rows(FROM="vim_actions", WHERE={"instance_action_id": instance_action_id,
+ tasks = self.db.get_rows(FROM="vim_wim_actions", WHERE={"instance_action_id": instance_action_id,
"task_index": task_index})
if not tasks:
return None
diff --git a/osm_ro/vimconn_openstack.py b/osm_ro/vimconn_openstack.py
index ffe2da0..876fa2f 100644
--- a/osm_ro/vimconn_openstack.py
+++ b/osm_ro/vimconn_openstack.py
@@ -44,6 +44,8 @@
import random
import re
import copy
+from pprint import pformat
+from types import StringTypes
from novaclient import client as nClient, exceptions as nvExceptions
from keystoneauth1.identity import v2, v3
@@ -77,6 +79,18 @@
volume_timeout = 600
server_timeout = 600
+
+class SafeDumper(yaml.SafeDumper):
+ def represent_data(self, data):
+ # Openstack APIs use custom subclasses of dict and YAML safe dumper
+ # is designed to not handle that (reference issue 142 of pyyaml)
+ if isinstance(data, dict) and data.__class__ != dict:
+ # A simple solution is to convert those items back to dicts
+ data = dict(data.items())
+
+ return super(SafeDumper, self).represent_data(data)
+
+
class vimconnector(vimconn.vimconnector):
def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None,
log_level=None, config={}, persistent_info={}):
@@ -158,11 +172,30 @@
vimconn.vimconnector.__setitem__(self, index, value)
self.session['reload_client'] = True
+ def serialize(self, value):
+ """Serialization of python basic types.
+
+ In the case value is not serializable a message will be logged and a
+ simple representation of the data that cannot be converted back to
+ python is returned.
+ """
+ if isinstance(value, StringTypes):
+ return value
+
+ try:
+ return yaml.dump(value, Dumper=SafeDumper,
+ default_flow_style=True, width=256)
+ except yaml.representer.RepresenterError:
+ self.logger.debug(
+ 'The following entity cannot be serialized in YAML:'
+ '\n\n%s\n\n', pformat(value), exc_info=True)
+ return str(value)
+
def _reload_connection(self):
'''Called before any operation, it check if credentials has changed
Throw keystoneclient.apiclient.exceptions.AuthorizationFailure
'''
- #TODO control the timing and possible token timeout, but it seams that python client does this task for us :-)
+ #TODO control the timing and possible token timeout, but it seams that python client does this task for us :-)
if self.session['reload_client']:
if self.config.get('APIversion'):
self.api_version3 = self.config['APIversion'] == 'v3.3' or self.config['APIversion'] == '3'
@@ -563,13 +596,13 @@
net_id: #VIM id of this network
status: #Mandatory. Text with one of:
# DELETED (not found at vim)
- # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
+ # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
# OTHER (Vim reported other status not understood)
# ERROR (VIM indicates an ERROR status)
- # ACTIVE, INACTIVE, DOWN (admin down),
+ # ACTIVE, INACTIVE, DOWN (admin down),
# BUILD (on building process)
#
- error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
+ error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
'''
@@ -586,10 +619,9 @@
if net['status'] == "ACTIVE" and not net_vim['admin_state_up']:
net['status'] = 'DOWN'
- try:
- net['vim_info'] = yaml.safe_dump(net_vim, default_flow_style=True, width=256)
- except yaml.representer.RepresenterError:
- net['vim_info'] = str(net_vim)
+
+ net['vim_info'] = self.serialize(net_vim)
+
if net_vim.get('fault'): #TODO
net['error_msg'] = str(net_vim['fault'])
except vimconn.vimconnNotFoundException as e:
@@ -1271,13 +1303,13 @@
Params:
vm_id: uuid of the VM
console_type, can be:
- "novnc" (by default), "xvpvnc" for VNC types,
+ "novnc" (by default), "xvpvnc" for VNC types,
"rdp-html5" for RDP types, "spice-html5" for SPICE types
Returns dict with the console parameters:
protocol: ssh, ftp, http, https, ...
- server: usually ip address
- port: the http, ssh, ... port
- suffix: extra text, e.g. the http path and query string
+ server: usually ip address
+ port: the http, ssh, ... port
+ suffix: extra text, e.g. the http path and query string
'''
self.logger.debug("Getting VM CONSOLE from VIM")
try:
@@ -1377,14 +1409,14 @@
vm_id: #VIM id of this Virtual Machine
status: #Mandatory. Text with one of:
# DELETED (not found at vim)
- # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
+ # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
# OTHER (Vim reported other status not understood)
# ERROR (VIM indicates an ERROR status)
- # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
+ # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
# CREATING (on building process), ERROR
# ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
#
- error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
+ error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
interfaces:
- vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
@@ -1407,10 +1439,9 @@
else:
vm['status'] = "OTHER"
vm['error_msg'] = "VIM status reported " + vm_vim['status']
- try:
- vm['vim_info'] = yaml.safe_dump(vm_vim, default_flow_style=True, width=256)
- except yaml.representer.RepresenterError:
- vm['vim_info'] = str(vm_vim)
+
+ vm['vim_info'] = self.serialize(vm_vim)
+
vm["interfaces"] = []
if vm_vim.get('fault'):
vm['error_msg'] = str(vm_vim['fault'])
@@ -1420,20 +1451,17 @@
port_dict = self.neutron.list_ports(device_id=vm_id)
for port in port_dict["ports"]:
interface={}
- try:
- interface['vim_info'] = yaml.safe_dump(port, default_flow_style=True, width=256)
- except yaml.representer.RepresenterError:
- interface['vim_info'] = str(port)
+ interface['vim_info'] = self.serialize(port)
interface["mac_address"] = port.get("mac_address")
interface["vim_net_id"] = port["network_id"]
interface["vim_interface_id"] = port["id"]
- # check if OS-EXT-SRV-ATTR:host is there,
+ # check if OS-EXT-SRV-ATTR:host is there,
# in case of non-admin credentials, it will be missing
if vm_vim.get('OS-EXT-SRV-ATTR:host'):
interface["compute_node"] = vm_vim['OS-EXT-SRV-ATTR:host']
interface["pci"] = None
- # check if binding:profile is there,
+ # check if binding:profile is there,
# in case of non-admin credentials, it will be missing
if port.get('binding:profile'):
if port['binding:profile'].get('pci_slot'):
diff --git a/osm_ro/wim/__init__.py b/osm_ro/wim/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/osm_ro/wim/__init__.py
diff --git a/osm_ro/wim/actions.py b/osm_ro/wim/actions.py
new file mode 100644
index 0000000..f224460
--- /dev/null
+++ b/osm_ro/wim/actions.py
@@ -0,0 +1,423 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+# pylint: disable=E1101,E0203,W0201
+
+"""Common logic for task management"""
+import logging
+from time import time
+from types import StringTypes
+
+from six.moves import range
+
+import yaml
+
+from ..utils import (
+ filter_dict_keys,
+ filter_out_dict_keys,
+ merge_dicts,
+ remove_none_items,
+ truncate
+)
+
+PENDING, REFRESH, IGNORE = range(3)
+
+TIMEOUT = 1 * 60 * 60 # 1 hour
+MIN_ATTEMPTS = 10
+
+
+class Action(object):
+ """Create a basic object representing the action record.
+
+ Arguments:
+ record (dict): record as returned by the database
+ **kwargs: extra keyword arguments to overwrite the fields in record
+ """
+
+ PROPERTIES = [
+ 'task_index', # MD - Index number of the task.
+ # This together with the instance_action_id
+ # forms a unique key identifier
+ 'action', # MD - CREATE, DELETE, FIND
+ 'item', # MD - table name, eg. instance_wim_nets
+ 'item_id', # MD - uuid of the referenced entry in the
+ # previous table
+ 'instance_action_id', # MD - reference to a cohesive group of actions
+ # related to the same instance-scenario
+ 'wim_account_id', # MD - reference to the WIM account used
+ # by the thread/connector
+ 'wim_internal_id', # MD - internal ID used by the WIM to refer to
+ # the item
+ 'datacenter_vim_id', # MD - reference to the VIM account used
+ # by the thread/connector
+ 'vim_id', # MD - internal ID used by the VIM to refer to
+ # the item
+ 'status', # MD - SCHEDULED,BUILD,DONE,FAILED,SUPERSEDED
+ 'extra', # MD - text with yaml format at database,
+ # dict at memory with:
+ # `- params: list with the params to be sent to the VIM for CREATE
+ # or FIND. For DELETE the vim_id is taken from other
+ # related tasks
+ # `- find: (only for CREATE tasks) if present it should FIND
+ # before creating and use if existing.
+ # Contains the FIND params
+ # `- depends_on: list with the 'task_index'es of tasks that must be
+ # completed before. e.g. a vm creation depends on a net
+ # creation
+ # `- sdn_net_id: used for net.
+ # `- tries
+ # `- created_items:
+ # dictionary with extra elements created that need
+ # to be deleted. e.g. ports,
+ # `- volumes,...
+ # `- created: False if the VIM element is not created by
+ # other actions, and it should not be deleted
+ # `- wim_status: WIM status of the element. Stored also at database
+ # in the item table
+ 'params', # M - similar to extra[params]
+ 'depends_on', # M - similar to extra[depends_on]
+ 'depends', # M - dict with task_index(from depends_on) to
+ # task class
+ 'error_msg', # MD - descriptive text upon an error
+ 'created_at', # MD - task DB creation time
+ 'modified_at', # MD - last DB update time
+ 'process_at', # M - unix epoch when to process the task
+ ]
+
+ __slots__ = PROPERTIES + [
+ 'logger',
+ ]
+
+ def __init__(self, record, logger=None, **kwargs):
+ self.logger = logger or logging.getLogger('openmano.wim.action')
+ attrs = merge_dicts(dict.fromkeys(self.PROPERTIES), record, kwargs)
+ self.update(_expand_extra(attrs))
+
+ def __repr__(self):
+ return super(Action, self).__repr__() + repr(self.as_dict())
+
+ def as_dict(self, *fields):
+ """Representation of the object as a dict"""
+ attrs = (set(self.PROPERTIES) & set(fields)
+ if fields else self.PROPERTIES)
+ return {k: getattr(self, k) for k in attrs}
+
+ def as_record(self):
+ """Returns a dict that can be send to the persistence layer"""
+ special = ['params', 'depends_on', 'depends']
+ record = self.as_dict()
+ record['extra'].update(self.as_dict(*special))
+ non_fields = special + ['process_at']
+
+ return remove_none_items(filter_out_dict_keys(record, non_fields))
+
+ def update(self, values=None, **kwargs):
+ """Update the in-memory representation of the task (works similarly to
+ dict.update). The update is NOT automatically persisted.
+ """
+ # "white-listed mass assignment"
+ updates = merge_dicts(values, kwargs)
+ for attr in set(self.PROPERTIES) & set(updates.keys()):
+ setattr(self, attr, updates[attr])
+
+ def save(self, persistence, **kwargs):
+ """Persist current state of the object to the database.
+
+ Arguments:
+ persistence: object encapsulating the database
+ **kwargs: extra properties to be updated before saving
+
+ Note:
+ If any key word argument is passed, the object itself will be
+ changed as an extra side-effect.
+ """
+ action_id = self.instance_action_id
+ index = self.task_index
+ if kwargs:
+ self.update(kwargs)
+ properties = self.as_record()
+
+ return persistence.update_action(action_id, index, properties)
+
+ def fail(self, persistence, reason, status='FAILED'):
+ """Mark action as FAILED, updating tables accordingly"""
+ persistence.update_instance_action_counters(
+ self.instance_action_id,
+ failed=1,
+ done=(-1 if self.status == 'DONE' else 0))
+
+ self.status = status
+ self.error_msg = truncate(reason)
+ self.logger.error('%s %s: %s', self.id, status, reason)
+ return self.save(persistence)
+
+ def succeed(self, persistence, status='DONE'):
+ """Mark action as DONE, updating tables accordingly"""
+ persistence.update_instance_action_counters(
+ self.instance_action_id, done=1)
+ self.status = status
+ self.logger.debug('%s %s', self.id, status)
+ return self.save(persistence)
+
+ def defer(self, persistence, reason,
+ timeout=TIMEOUT, min_attempts=MIN_ATTEMPTS):
+ """Postpone the task processing, taking care to not timeout.
+
+ Arguments:
+ persistence: object encapsulating the database
+ reason (str): explanation for the delay
+ timeout (int): maximum delay tolerated since the first attempt.
+ Note that this number is a time delta, in seconds
+ min_attempts (int): Number of attempts to try before giving up.
+ """
+ now = time()
+ last_attempt = self.extra.get('last_attempted_at') or time()
+ attempts = self.extra.get('attempts') or 0
+
+ if last_attempt - now > timeout and attempts > min_attempts:
+ self.fail(persistence,
+ 'Timeout reached. {} attempts in the last {:d} min'
+ .format(attempts, last_attempt / 60))
+
+ self.extra['last_attempted_at'] = time()
+ self.extra['attempts'] = attempts + 1
+ self.logger.info('%s DEFERRED: %s', self.id, reason)
+ return self.save(persistence)
+
+ @property
+ def group_key(self):
+ """Key defining the group to which this tasks belongs"""
+ return (self.item, self.item_id)
+
+ @property
+ def processing(self):
+ """Processing status for the task (PENDING, REFRESH, IGNORE)"""
+ if self.status == 'SCHEDULED':
+ return PENDING
+
+ return IGNORE
+
+ @property
+ def id(self):
+ """Unique identifier of this particular action"""
+ return '{}[{}]'.format(self.instance_action_id, self.task_index)
+
+ @property
+ def is_scheduled(self):
+ return self.status == 'SCHEDULED'
+
+ @property
+ def is_build(self):
+ return self.status == 'BUILD'
+
+ @property
+ def is_done(self):
+ return self.status == 'DONE'
+
+ @property
+ def is_failed(self):
+ return self.status == 'FAILED'
+
+ @property
+ def is_superseded(self):
+ return self.status == 'SUPERSEDED'
+
+ def refresh(self, connector, persistence):
+ """Use the connector/persistence to refresh the status of the item.
+
+ After the item status is refreshed any change in the task should be
+ persisted to the database.
+
+ Arguments:
+ connector: object containing the classes to access the WIM or VIM
+ persistence: object containing the methods necessary to query the
+ database and to persist the updates
+ """
+ self.logger.debug(
+ 'Action `%s` has no refresh to be done',
+ self.__class__.__name__)
+
+ def expand_dependency_links(self, task_group):
+ """Expand task indexes into actual IDs"""
+ if not self.depends_on or (
+ isinstance(self.depends, dict) and self.depends):
+ return
+
+ num_tasks = len(task_group)
+ references = {
+ "TASK-{}".format(i): task_group[i]
+ for i in self.depends_on
+ if i < num_tasks and task_group[i].task_index == i and
+ task_group[i].instance_action_id == self.instance_action_id
+ }
+ self.depends = references
+
+ def become_superseded(self, superseding):
+ """When another action tries to supersede this one,
+ we need to change both of them, so the surviving actions will be
+ logic consistent.
+
+ This method should do the required internal changes, and also
+ suggest changes for the other, superseding, action.
+
+ Arguments:
+ superseding: other task superseding this one
+
+ Returns:
+ dict: changes suggested to the action superseding this one.
+ A special key ``superseding_needed`` is used to
+ suggest if the superseding is actually required or not.
+ If not present, ``superseding_needed`` is assumed to
+ be False.
+ """
+ self.status = 'SUPERSEDED'
+ self.logger.debug(
+ 'Action `%s` was superseded by `%s`',
+ self.__class__.__name__, superseding.__class__.__name__)
+ return {}
+
+ def supersede(self, others):
+ """Supersede other tasks, if necessary
+
+ Arguments:
+ others (list): action objects being superseded
+
+ When the task decide to supersede others, this method should call
+ ``become_superseded`` on the other actions, collect the suggested
+ updates and perform the necessary changes
+ """
+ # By default actions don't supersede others
+ self.logger.debug(
+ 'Action `%s` does not supersede other actions',
+ self.__class__.__name__)
+
+ def process(self, connector, persistence, ovim):
+ """Abstract method, that needs to be implemented.
+ Process the current task.
+
+ Arguments:
+ connector: object with API for accessing the WAN
+ Infrastructure Manager system
+ persistence: abstraction layer for the database
+ ovim: instance of openvim, abstraction layer that enable
+ SDN-related operations
+ """
+ raise NotImplementedError
+
+
+class FindAction(Action):
+ """Abstract class that should be inherited for FIND actions, depending on
+ the item type.
+ """
+ @property
+ def processing(self):
+ if self.status in ('DONE', 'BUILD'):
+ return REFRESH
+
+ return super(FindAction, self).processing
+
+ def become_superseded(self, superseding):
+ super(FindAction, self).become_superseded(superseding)
+ info = ('vim_id', 'wim_internal_id')
+ return remove_none_items({f: getattr(self, f) for f in info})
+
+
+class CreateAction(Action):
+ """Abstract class that should be inherited for CREATE actions, depending on
+ the item type.
+ """
+ @property
+ def processing(self):
+ if self.status in ('DONE', 'BUILD'):
+ return REFRESH
+
+ return super(CreateAction, self).processing
+
+ def become_superseded(self, superseding):
+ super(CreateAction, self).become_superseded(superseding)
+
+ created = self.extra.get('created', True)
+ sdn_net_id = self.extra.get('sdn_net_id')
+ pending_info = self.wim_internal_id or self.vim_id or sdn_net_id
+ if not(created and pending_info):
+ return {}
+
+ extra_fields = ('sdn_net_id', 'interfaces', 'created_items')
+ extra_info = filter_dict_keys(self.extra or {}, extra_fields)
+
+ return {'superseding_needed': True,
+ 'wim_internal_id': self.wim_internal_id,
+ 'vim_id': self.vim_id,
+ 'extra': remove_none_items(extra_info)}
+
+
+class DeleteAction(Action):
+ """Abstract class that should be inherited for DELETE actions, depending on
+ the item type.
+ """
+ def supersede(self, others):
+ self.logger.debug('%s %s %s %s might supersede other actions',
+ self.id, self.action, self.item, self.item_id)
+ # First collect all the changes from the superseded tasks
+ changes = [other.become_superseded(self) for other in others]
+ needed = any(change.pop('superseding_needed', False)
+ for change in changes)
+
+ # Deal with the nested ones first
+ extras = [change.pop('extra', None) or {} for change in changes]
+ items = [extra.pop('created_items', None) or {} for extra in extras]
+ items = merge_dicts(self.extra.get('created_items', {}), *items)
+ self.extra = merge_dicts(self.extra, {'created_items': items}, *extras)
+
+ # Accept the other ones
+ change = ((key, value) for key, value in merge_dicts(*changes).items()
+ if key in self.PROPERTIES)
+ for attr, value in change:
+ setattr(self, attr, value)
+
+ # Reevaluate if the action itself is needed
+ if not needed:
+ self.status = 'SUPERSEDED'
+
+
+def _expand_extra(record):
+ extra = record.pop('extra', None) or {}
+ if isinstance(extra, StringTypes):
+ extra = yaml.safe_load(extra)
+
+ record['params'] = extra.get('params')
+ record['depends_on'] = extra.get('depends_on', [])
+ record['depends'] = extra.get('depends', None)
+ record['extra'] = extra
+
+ return record
diff --git a/osm_ro/wim/engine.py b/osm_ro/wim/engine.py
new file mode 100644
index 0000000..dde5dee
--- /dev/null
+++ b/osm_ro/wim/engine.py
@@ -0,0 +1,455 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+"""This module contains the domain logic, and the implementation of the
+required steps to perform VNF management and orchestration in a WAN
+environment.
+
+It works as an extension/complement to the main functions contained in the
+``nfvo.py`` file and avoids interacting directly with the database, by relying
+on the `persistence` module.
+
+No http request handling/direct interaction with the database should be present
+in this file.
+"""
+import json
+import logging
+from contextlib import contextmanager
+from itertools import groupby
+from operator import itemgetter
+from uuid import uuid4
+
+from ..utils import remove_none_items
+from .actions import Action
+from .errors import (
+ NoWimConnectedToDatacenters,
+ WimAccountNotActive
+)
+from .wim_thread import WimThread
+
+
+class WimEngine(object):
+ """Logic supporting the establishment of WAN links when NS spans across
+ different datacenters.
+ """
+ def __init__(self, persistence, logger=None, ovim=None):
+ self.persist = persistence
+ self.logger = logger or logging.getLogger('openmano.wim.engine')
+ self.threads = {}
+ self.connectors = {}
+ self.ovim = ovim
+
+ def create_wim(self, properties):
+ """Create a new wim record according to the properties
+
+ Please check the wim schema to have more information about
+ ``properties``.
+
+ Returns:
+ str: uuid of the newly created WIM record
+ """
+ return self.persist.create_wim(properties)
+
+ def get_wim(self, uuid_or_name, tenant_id=None):
+ """Retrieve existing WIM record by name or id.
+
+ If ``tenant_id`` is specified, the query will be
+ limited to the WIM associated to the given tenant.
+ """
+ # Since it is a pure DB operation, we can delegate it directly
+ return self.persist.get_wim(uuid_or_name, tenant_id)
+
+ def update_wim(self, uuid_or_name, properties):
+ """Edit an existing WIM record.
+
+ ``properties`` is a dictionary with the properties being changed,
+ if a property is not present, the old value will be preserved
+ """
+ return self.persist.update_wim(uuid_or_name, properties)
+
+ def delete_wim(self, uuid_or_name):
+ """Kill the corresponding wim threads and erase the WIM record"""
+ # Theoretically, we can rely on the database to drop the wim_accounts
+ # automatically, since we have configures 'ON CASCADE DELETE'.
+ # However, use use `delete_wim_accounts` to kill all the running
+ # threads.
+ self.delete_wim_accounts(uuid_or_name)
+ return self.persist.delete_wim(uuid_or_name)
+
+ def create_wim_account(self, wim, tenant, properties):
+ """Create an account that associates a tenant to a WIM.
+
+ As a side effect this function will spawn a new thread
+
+ Arguments:
+ wim (str): name or uuid of the WIM related to the account being
+ created
+ tenant (str): name or uuid of the nfvo tenant to which the account
+ will be created
+ properties (dict): properties of the account
+ (eg. username, password, ...)
+
+ Returns:
+ dict: Created record
+ """
+ uuid = self.persist.create_wim_account(wim, tenant, properties)
+ account = self.persist.get_wim_account_by(uuid=uuid)
+ # ^ We need to use get_wim_account_by here, since this methods returns
+ # all the associations, and we need the wim to create the thread
+ self._spawn_thread(account)
+ return account
+
+ def _update_single_wim_account(self, account, properties):
+ """Update WIM Account, taking care to reload the corresponding thread
+
+ Arguments:
+ account (dict): Current account record
+ properties (dict): Properties to be updated
+
+ Returns:
+ dict: updated record
+ """
+ account = self.persist.update_wim_account(account['uuid'], properties)
+ self.threads[account['uuid']].reload()
+ return account
+
+ def update_wim_accounts(self, wim, tenant, properties):
+ """Update all the accounts related to a WIM and a tenant,
+ thanking care of reloading threads.
+
+ Arguments:
+ wim (str): uuid or name of a WIM record
+ tenant (str): uuid or name of a NFVO tenant record
+ properties (dict): attributes with values to be updated
+
+ Returns
+ list: Records that were updated
+ """
+ accounts = self.persist.get_wim_accounts_by(wim, tenant)
+ return [self._update_single_wim_account(account, properties)
+ for account in accounts]
+
+ def _delete_single_wim_account(self, account):
+ """Delete WIM Account, taking care to remove the corresponding thread
+ and delete the internal WIM account, if it was automatically generated.
+
+ Arguments:
+ account (dict): Current account record
+ properties (dict): Properties to be updated
+
+ Returns:
+ dict: current record (same as input)
+ """
+ self.persist.delete_wim_account(account['uuid'])
+
+ if account['uuid'] not in self.threads:
+ raise WimAccountNotActive(
+ 'Requests send to the WIM Account %s are not currently '
+ 'being processed.', account['uuid'])
+ else:
+ self.threads[account['uuid']].exit()
+ del self.threads[account['uuid']]
+
+ return account
+
+ def delete_wim_accounts(self, wim, tenant=None, **kwargs):
+ """Delete all the accounts related to a WIM (and a tenant),
+ thanking care of threads and internal WIM accounts.
+
+ Arguments:
+ wim (str): uuid or name of a WIM record
+ tenant (str): uuid or name of a NFVO tenant record
+
+ Returns
+ list: Records that were deleted
+ """
+ kwargs.setdefault('error_if_none', False)
+ accounts = self.persist.get_wim_accounts_by(wim, tenant, **kwargs)
+ return [self._delete_single_wim_account(a) for a in accounts]
+
+ def _reload_wim_threads(self, wim_id):
+ for thread in self.threads.values():
+ if thread.wim_account['wim_id'] == wim_id:
+ thread.reload()
+
+ def create_wim_port_mappings(self, wim, properties, tenant=None):
+ """Store information about port mappings from Database"""
+ # TODO: Review tenants... WIMs can exist across different tenants,
+ # and the port_mappings are a WIM property, not a wim_account
+ # property, so the concepts are not related
+ wim = self.persist.get_by_name_or_uuid('wims', wim)
+ result = self.persist.create_wim_port_mappings(wim, properties, tenant)
+ self._reload_wim_threads(wim['uuid'])
+ return result
+
+ def get_wim_port_mappings(self, wim):
+ """Retrive information about port mappings from Database"""
+ return self.persist.get_wim_port_mappings(wim)
+
+ def delete_wim_port_mappings(self, wim):
+ """Erase the port mapping records associated with the WIM"""
+ wim = self.persist.get_by_name_or_uuid('wims', wim)
+ message = self.persist.delete_wim_port_mappings(wim['uuid'])
+ self._reload_wim_threads(wim['uuid'])
+ return message
+
+ def find_common_wims(self, datacenter_ids, tenant):
+ """Find WIMs that are common to all datacenters listed"""
+ mappings = self.persist.get_wim_port_mappings(
+ datacenter=datacenter_ids, tenant=tenant, error_if_none=False)
+
+ wim_id_of = itemgetter('wim_id')
+ sorted_mappings = sorted(mappings, key=wim_id_of) # needed by groupby
+ grouped_mappings = groupby(sorted_mappings, key=wim_id_of)
+ mapped_datacenters = {
+ wim_id: [m['datacenter_id'] for m in mappings]
+ for wim_id, mappings in grouped_mappings
+ }
+
+ return [
+ wim_id
+ for wim_id, connected_datacenters in mapped_datacenters.items()
+ if set(connected_datacenters) >= set(datacenter_ids)
+ ]
+
+ def find_common_wim(self, datacenter_ids, tenant):
+ """Find a single WIM that is able to connect all the datacenters
+ listed
+
+ Raises
+ NoWimConnectedToDatacenters: if no WIM connected to all datacenters
+ at once is found
+ """
+ suitable_wim_ids = self.find_common_wims(datacenter_ids, tenant)
+
+ if not suitable_wim_ids:
+ raise NoWimConnectedToDatacenters(datacenter_ids)
+
+ # TODO: use a criteria to determine which WIM is going to be used,
+ # instead of always using the first one (strategy pattern can be
+ # used here)
+ return suitable_wim_ids[0]
+
+ def derive_wan_link(self,
+ instance_scenario_id, sce_net_id,
+ networks, tenant):
+ """Create a instance_wim_nets record for the given information"""
+ datacenters = [n['datacenter_id'] for n in networks]
+ wim_id = self.find_common_wim(datacenters, tenant)
+
+ account = self.persist.get_wim_account_by(wim_id, tenant)
+
+ return {
+ 'uuid': str(uuid4()),
+ 'instance_scenario_id': instance_scenario_id,
+ 'sce_net_id': sce_net_id,
+ 'wim_id': wim_id,
+ 'wim_account_id': account['uuid']
+ }
+
+ def derive_wan_links(self, networks, tenant=None):
+ """Discover and return what are the wan_links that have to be created
+ considering a set of networks (VLDs) required for a scenario instance
+ (NSR).
+
+ Arguments:
+ networks(list): Dicts containing the information about the networks
+ that will be instantiated to materialize a Network Service
+ (scenario) instance.
+
+ Returns:
+ list: list of WAN links to be written to the database
+ """
+ # Group networks by key=(instance_scenario_id, sce_net_id)
+ filtered = _filter_multi_vim(networks)
+ grouped_networks = _group_networks(filtered)
+ datacenters_per_group = _count_datacenters(grouped_networks)
+ # For each group count the number of networks. If greater then 1,
+ # we have to create a wan link connecting them.
+ wan_groups = [key
+ for key, counter in datacenters_per_group
+ if counter > 1]
+
+ return [
+ self.derive_wan_link(key[0], key[1], grouped_networks[key], tenant)
+ for key in wan_groups
+ ]
+
+ def create_action(self, wan_link):
+ """For a single wan_link create the corresponding create action"""
+ return {
+ 'action': 'CREATE',
+ 'status': 'SCHEDULED',
+ 'item': 'instance_wim_nets',
+ 'item_id': wan_link['uuid'],
+ 'wim_account_id': wan_link['wim_account_id']
+ }
+
+ def create_actions(self, wan_links):
+ """For an array of wan_links, create all the corresponding actions"""
+ return [self.create_action(l) for l in wan_links]
+
+ def delete_action(self, wan_link):
+ """For a single wan_link create the corresponding create action"""
+ return {
+ 'action': 'DELETE',
+ 'status': 'SCHEDULED',
+ 'item': 'instance_wim_nets',
+ 'item_id': wan_link['uuid'],
+ 'wim_account_id': wan_link['wim_account_id'],
+ 'extra': json.dumps({'wan_link': wan_link})
+ # We serialize and cache the wan_link here, because it can be
+ # deleted during the delete process
+ }
+
+ def delete_actions(self, wan_links=(), instance_scenario_id=None):
+ """Given a Instance Scenario, remove all the WAN Links created in the
+ past"""
+ if instance_scenario_id:
+ wan_links = self.persist.get_wan_links(
+ instance_scenario_id=instance_scenario_id)
+ return [self.delete_action(l) for l in wan_links]
+
+ def incorporate_actions(self, wim_actions, instance_action):
+ """Make the instance action consider new WIM actions and make the WIM
+ actions aware of the instance action
+ """
+ current = instance_action.setdefault('number_tasks', 0)
+ for i, action in enumerate(wim_actions):
+ action['task_index'] = current + i
+ action['instance_action_id'] = instance_action['uuid']
+ instance_action['number_tasks'] += len(wim_actions)
+
+ return wim_actions, instance_action
+
+ def dispatch(self, tasks):
+ """Enqueue a list of tasks for further processing.
+
+ This function is supposed to be called outside from the WIM Thread.
+ """
+ for task in tasks:
+ if task['wim_account_id'] not in self.threads:
+ error_msg = str(WimAccountNotActive(
+ 'Requests send to the WIM Account %s are not currently '
+ 'being processed.', task['wim_account_id']))
+ Action(task, self.logger).fail(self.persist, error_msg)
+ self.persist.update_wan_link(task['item_id'],
+ {'status': 'ERROR',
+ 'error_msg': error_msg})
+ self.logger.error('Task %s %s %s not dispatched.\n%s',
+ task['action'], task['item'],
+ task['instance_account_id'], error_msg)
+ else:
+ self.threads[task['wim_account_id']].insert_task(task)
+ self.logger.debug('Task %s %s %s dispatched',
+ task['action'], task['item'],
+ task['instance_action_id'])
+
+ def _spawn_thread(self, wim_account):
+ """Spawn a WIM thread
+
+ Arguments:
+ wim_account (dict): WIM information (usually persisted)
+ The `wim` field is required to be set with a valid WIM record
+ inside the `wim_account` dict
+
+ Return:
+ threading.Thread: Thread object
+ """
+ thread = None
+ try:
+ thread = WimThread(self.persist, wim_account, ovim=self.ovim)
+ self.threads[wim_account['uuid']] = thread
+ thread.start()
+ except: # noqa
+ self.logger.error('Error when spawning WIM thread for %s',
+ wim_account['uuid'], exc_info=True)
+
+ return thread
+
+ def start_threads(self):
+ """Start the threads responsible for processing WIM Actions"""
+ accounts = self.persist.get_wim_accounts(error_if_none=False)
+ self.threads = remove_none_items(
+ {a['uuid']: self._spawn_thread(a) for a in accounts})
+
+ def stop_threads(self):
+ """Stop the threads responsible for processing WIM Actions"""
+ for uuid, thread in self.threads.items():
+ thread.exit()
+ del self.threads[uuid]
+
+ @contextmanager
+ def threads_running(self):
+ """Ensure no thread will be left running"""
+ # This method is particularly important for testing :)
+ try:
+ self.start_threads()
+ yield
+ finally:
+ self.stop_threads()
+
+
+def _filter_multi_vim(networks):
+ """Ignore networks without sce_net_id (all VNFs go to the same VIM)"""
+ return [n for n in networks if 'sce_net_id' in n and n['sce_net_id']]
+
+
+def _group_networks(networks):
+ """Group networks that correspond to the same instance_scenario_id and
+ sce_net_id (NSR and VLD).
+
+ Arguments:
+ networks(list): Dicts containing the information about the networks
+ that will be instantiated to materialize a Network Service
+ (scenario) instance.
+ Returns:
+ dict: Keys are tuples (instance_scenario_id, sce_net_id) and values
+ are lits of networks.
+ """
+ criteria = itemgetter('instance_scenario_id', 'sce_net_id')
+
+ networks = sorted(networks, key=criteria)
+ return {k: list(v) for k, v in groupby(networks, key=criteria)}
+
+
+def _count_datacenters(grouped_networks):
+ """Count the number of datacenters in each group of networks
+
+ Returns:
+ list of tuples: the first element is the group key, while the second
+ element is the number of datacenters in each group.
+ """
+ return ((key, len(set(n['datacenter_id'] for n in group)))
+ for key, group in grouped_networks.items())
diff --git a/osm_ro/wim/errors.py b/osm_ro/wim/errors.py
new file mode 100644
index 0000000..16c53b5
--- /dev/null
+++ b/osm_ro/wim/errors.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+from six.moves import queue
+
+from ..db_base import db_base_Exception as DbBaseException
+from ..http_tools.errors import (
+ Bad_Request,
+ Conflict,
+ HttpMappedError,
+ Internal_Server_Error,
+ Not_Found
+)
+
+
+class NoRecordFound(DbBaseException):
+ """No record was found in the database"""
+
+ def __init__(self, criteria, table=None):
+ table_info = '{} - '.format(table) if table else ''
+ super(NoRecordFound, self).__init__(
+ '{}: {}`{}`'.format(self.__class__.__doc__, table_info, criteria),
+ http_code=Not_Found)
+
+
+class MultipleRecordsFound(DbBaseException):
+ """More than one record was found in the database"""
+
+ def __init__(self, criteria, table=None):
+ table_info = '{} - '.format(table) if table else ''
+ super(MultipleRecordsFound, self).__init__(
+ '{}: {}`{}`'.format(self.__class__.__doc__, table_info, criteria),
+ http_code=Conflict)
+
+
+class WimAndTenantNotAttached(DbBaseException):
+ """Wim and Tenant are not attached"""
+
+ def __init__(self, wim, tenant):
+ super(WimAndTenantNotAttached, self).__init__(
+ '{}: `{}` <> `{}`'.format(self.__class__.__doc__, wim, tenant),
+ http_code=Conflict)
+
+
+class WimAndTenantAlreadyAttached(DbBaseException):
+ """There is already a wim account attaching the given wim and tenant"""
+
+ def __init__(self, wim, tenant):
+ super(WimAndTenantAlreadyAttached, self).__init__(
+ '{}: `{}` <> `{}`'.format(self.__class__.__doc__, wim, tenant),
+ http_code=Conflict)
+
+
+class NoWimConnectedToDatacenters(NoRecordFound):
+ """No WIM that is able to connect the given datacenters was found"""
+
+
+class InvalidParameters(DbBaseException):
+ """The given parameters are invalid"""
+
+ def __init__(self, message, http_code=Bad_Request):
+ super(InvalidParameters, self).__init__(message, http_code)
+
+
+class UndefinedAction(HttpMappedError):
+ """No action found"""
+
+ def __init__(self, item_type, action, http_code=Internal_Server_Error):
+ message = ('The action {} {} is not defined'.format(action, item_type))
+ super(UndefinedAction, self).__init__(message, http_code)
+
+
+class UndefinedWimConnector(DbBaseException):
+ """The connector class for the specified wim type is not implemented"""
+
+ def __init__(self, wim_type, module_name, location_reference):
+ super(UndefinedWimConnector, self).__init__(
+ ('{}: `{}`. Could not find module `{}` '
+ '(check if it is necessary to install a plugin)'
+ .format(self.__class__.__doc__, wim_type, module_name)),
+ http_code=Bad_Request)
+
+
+class WimAccountOverwrite(DbBaseException):
+ """An attempt to overwrite an existing WIM account was identified"""
+
+ def __init__(self, wim_account, diff=None, tip=None):
+ message = self.__class__.__doc__
+ account_info = (
+ 'Account -- name: {name}, uuid: {uuid}'.format(**wim_account)
+ if wim_account else '')
+ diff_info = (
+ 'Differing fields: ' + ', '.join(diff.keys()) if diff else '')
+
+ super(WimAccountOverwrite, self).__init__(
+ '\n'.join(m for m in (message, account_info, diff_info, tip) if m),
+ http_code=Conflict)
+
+
+class UnexpectedDatabaseError(DbBaseException):
+ """The database didn't raised an exception but also the query was not
+ executed (maybe the connection had some problems?)
+ """
+
+
+class UndefinedUuidOrName(DbBaseException):
+ """Trying to query for a record using an empty uuid or name"""
+
+ def __init__(self, table=None):
+ table_info = '{} - '.format(table.split()[0]) if table else ''
+ super(UndefinedUuidOrName, self).__init__(
+ table_info + self.__class__.__doc__, http_status=Bad_Request)
+
+
+class UndefinedWanMappingType(InvalidParameters):
+ """The dict wan_service_mapping_info MUST contain a `type` field"""
+
+ def __init__(self, given):
+ super(UndefinedWanMappingType, self).__init__(
+ '{}. Given: `{}`'.format(self.__class__.__doc__, given))
+
+
+class QueueFull(HttpMappedError, queue.Full):
+ """Thread queue is full"""
+
+ def __init__(self, thread_name, http_code=Internal_Server_Error):
+ message = ('Thread {} queue is full'.format(thread_name))
+ super(QueueFull, self).__init__(message, http_code)
+
+
+class InconsistentState(HttpMappedError):
+ """An unexpected inconsistency was find in the state of the program"""
+
+ def __init__(self, arg, http_code=Internal_Server_Error):
+ if isinstance(arg, HttpMappedError):
+ http_code = arg.http_code
+ message = str(arg)
+ else:
+ message = arg
+
+ super(InconsistentState, self).__init__(message, http_code)
+
+
+class WimAccountNotActive(HttpMappedError, KeyError):
+ """WIM Account is not active yet (no thread is running)"""
+
+ def __init__(self, message, http_code=Internal_Server_Error):
+ message += ('\nThe thread responsible for processing the actions have '
+ 'suddenly stopped, or have never being spawned')
+ super(WimAccountNotActive, self).__init__(message, http_code)
diff --git a/osm_ro/wim/failing_connector.py b/osm_ro/wim/failing_connector.py
new file mode 100644
index 0000000..b66551c
--- /dev/null
+++ b/osm_ro/wim/failing_connector.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+"""In the case any error happens when trying to initiate the WIM Connector,
+we need a replacement for it, that will throw an error every time we try to
+execute any action
+"""
+import json
+from .wimconn import WimConnectorError
+
+
+class FailingConnector(object):
+ """Placeholder for a connector whose incitation failed,
+ This place holder will just raise an error every time an action is needed
+ from the connector.
+
+ This way we can make sure that all the other parts of the program will work
+ but the user will have all the information available to fix the problem.
+ """
+ def __init__(self, error_msg):
+ self.error_msg = error_msg
+
+ def check_credentials(self):
+ raise WimConnectorError('Impossible to use WIM:\n' + self.error_msg)
+
+ def get_connectivity_service_status(self, service_uuid, _conn_info=None):
+ raise WimConnectorError('Impossible to retrieve status for {}\n\n{}'
+ .format(service_uuid, self.error_msg))
+
+ def create_connectivity_service(self, service_uuid, *args, **kwargs):
+ raise WimConnectorError('Impossible to connect {}.\n{}\n{}\n{}'
+ .format(service_uuid, self.error_msg,
+ json.dumps(args, indent=4),
+ json.dumps(kwargs, indent=4)))
+
+ def delete_connectivity_service(self, service_uuid, _conn_info=None):
+ raise WimConnectorError('Impossible to disconnect {}\n\n{}'
+ .format(service_uuid, self.error_msg))
+
+ def edit_connectivity_service(self, service_uuid, *args, **kwargs):
+ raise WimConnectorError('Impossible to change connection {}.\n{}\n'
+ '{}\n{}'
+ .format(service_uuid, self.error_msg,
+ json.dumps(args, indent=4),
+ json.dumps(kwargs, indent=4)))
+
+ def clear_all_connectivity_services(self):
+ raise WimConnectorError('Impossible to use WIM:\n' + self.error_msg)
+
+ def get_all_active_connectivity_services(self):
+ raise WimConnectorError('Impossible to use WIM:\n' + self.error_msg)
diff --git a/osm_ro/wim/http_handler.py b/osm_ro/wim/http_handler.py
new file mode 100644
index 0000000..f5eeed9
--- /dev/null
+++ b/osm_ro/wim/http_handler.py
@@ -0,0 +1,223 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+"""This module works as an extension to the toplevel ``httpserver`` module,
+implementing callbacks for the HTTP routes related to the WIM features of OSM.
+
+Acting as a front-end, it is responsible for converting the HTTP request
+payload into native python objects, calling the correct engine methods
+and converting back the response objects into strings to be send in the HTTP
+response payload.
+
+Direct domain/persistence logic should be avoided in this file, instead
+calls to other layers should be done.
+"""
+import logging
+
+from bottle import request
+
+from .. import utils
+from ..http_tools.errors import ErrorHandler
+from ..http_tools.handler import BaseHandler, route
+from ..http_tools.request_processing import (
+ filter_query_string,
+ format_in,
+ format_out
+)
+from .engine import WimEngine
+from .persistence import WimPersistence
+from .schemas import (
+ wim_account_schema,
+ wim_edit_schema,
+ wim_port_mapping_schema,
+ wim_schema
+)
+
+
+class WimHandler(BaseHandler):
+ """HTTP route implementations for WIM related URLs
+
+ Arguments:
+ db: instance of mydb [optional]. This argument must be provided
+ if not ``persistence`` is passed
+ persistence (WimPersistence): High-level data storage abstraction
+ [optional]. If this argument is not present, ``db`` must be.
+ engine (WimEngine): Implementation of the business logic
+ for the engine of WAN networks
+ logger (logging.Logger): logger object [optional]
+ url_base(str): Path fragment to be prepended to the routes [optional]
+ plugins(list): List of bottle plugins to be applied to routes
+ [optional]
+ """
+ def __init__(self, db=None, persistence=None, engine=None,
+ url_base='', logger=None, plugins=()):
+ self.persist = persistence or WimPersistence(db)
+ self.engine = engine or WimEngine(self.persist)
+ self.url_base = url_base
+ self.logger = logger or logging.getLogger('openmano.wim.http')
+ error_handler = ErrorHandler(self.logger)
+ self.plugins = [error_handler] + list(plugins)
+
+ @route('GET', '/<tenant_id>/wims')
+ def http_list_wims(self, tenant_id):
+ allowed_fields = ('uuid', 'name', 'wim_url', 'type', 'created_at')
+ select_, where_, limit_ = filter_query_string(
+ request.query, None, allowed_fields)
+ # ^ Since we allow the user to customize the db query using the HTTP
+ # query and it is quite difficult to re-use this query, let's just
+ # do a ad-hoc call to the db
+
+ from_ = 'wims'
+ if tenant_id != 'any':
+ where_['nfvo_tenant_id'] = tenant_id
+ if 'created_at' in select_:
+ select_[select_.index('created_at')] = (
+ 'w.created_at as created_at')
+ if 'created_at' in where_:
+ where_['w.created_at'] = where_.pop('created_at')
+ from_ = ('wims as w join wim_nfvo_tenants as wt '
+ 'on w.uuid=wt.wim_id')
+
+ wims = self.persist.query(
+ FROM=from_, SELECT=select_, WHERE=where_, LIMIT=limit_,
+ error_if_none=False)
+
+ utils.convert_float_timestamp2str(wims)
+ return format_out({'wims': wims})
+
+ @route('GET', '/<tenant_id>/wims/<wim_id>')
+ def http_get_wim(self, tenant_id, wim_id):
+ tenant_id = None if tenant_id == 'any' else tenant_id
+ wim = self.engine.get_wim(wim_id, tenant_id)
+ return format_out({'wim': wim})
+
+ @route('POST', '/wims')
+ def http_create_wim(self):
+ http_content, _ = format_in(wim_schema, confidential_data=True)
+ r = utils.remove_extra_items(http_content, wim_schema)
+ if r:
+ self.logger.debug("Remove extra items received %r", r)
+ data = self.engine.create_wim(http_content['wim'])
+ return self.http_get_wim('any', data)
+
+ @route('PUT', '/wims/<wim_id>')
+ def http_update_wim(self, wim_id):
+ '''edit wim details, can use both uuid or name'''
+ # parse input data
+ http_content, _ = format_in(wim_edit_schema)
+ r = utils.remove_extra_items(http_content, wim_edit_schema)
+ if r:
+ self.logger.debug("Remove received extra items %s", r)
+
+ wim_id = self.engine.update_wim(wim_id, http_content['wim'])
+ return self.http_get_wim('any', wim_id)
+
+ @route('DELETE', '/wims/<wim_id>')
+ def http_delete_wim(self, wim_id):
+ """Delete a wim from a database, can use both uuid or name"""
+ data = self.engine.delete_wim(wim_id)
+ # TODO Remove WIM in orchestrator
+ return format_out({"result": "wim '" + data + "' deleted"})
+
+ @route('POST', '/<tenant_id>/wims/<wim_id>')
+ def http_create_wim_account(self, tenant_id, wim_id):
+ """Associate an existing wim to this tenant"""
+ # parse input data
+ http_content, _ = format_in(
+ wim_account_schema, confidential_data=True)
+ removed = utils.remove_extra_items(http_content, wim_account_schema)
+ removed and self.logger.debug("Remove extra items %r", removed)
+ account = self.engine.create_wim_account(
+ wim_id, tenant_id, http_content['wim_account'])
+ # check update succeeded
+ return format_out({"wim_account": account})
+
+ @route('PUT', '/<tenant_id>/wims/<wim_id>')
+ def http_update_wim_accounts(self, tenant_id, wim_id):
+ """Edit the association of an existing wim to this tenant"""
+ tenant_id = None if tenant_id == 'any' else tenant_id
+ # parse input data
+ http_content, _ = format_in(
+ wim_account_schema, confidential_data=True)
+ removed = utils.remove_extra_items(http_content, wim_account_schema)
+ removed and self.logger.debug("Remove extra items %r", removed)
+ accounts = self.engine.update_wim_accounts(
+ wim_id, tenant_id, http_content['wim_account'])
+
+ if tenant_id:
+ return format_out({'wim_account': accounts[0]})
+
+ return format_out({'wim_accounts': accounts})
+
+ @route('DELETE', '/<tenant_id>/wims/<wim_id>')
+ def http_delete_wim_accounts(self, tenant_id, wim_id):
+ """Deassociate an existing wim to this tenant"""
+ tenant_id = None if tenant_id == 'any' else tenant_id
+ accounts = self.engine.delete_wim_accounts(wim_id, tenant_id,
+ error_if_none=True)
+
+ properties = (
+ (account['name'], wim_id,
+ utils.safe_get(account, 'association.nfvo_tenant_id', tenant_id))
+ for account in accounts)
+
+ return format_out({
+ 'result': '\n'.join('WIM account `{}` deleted. '
+ 'Tenant `{}` detached from WIM `{}`'
+ .format(*p) for p in properties)
+ })
+
+ @route('POST', '/<tenant_id>/wims/<wim_id>/port_mapping')
+ def http_create_wim_port_mappings(self, tenant_id, wim_id):
+ """Set the wim port mapping for a wim"""
+ # parse input data
+ http_content, _ = format_in(wim_port_mapping_schema)
+
+ data = self.engine.create_wim_port_mappings(
+ wim_id, http_content['wim_port_mapping'], tenant_id)
+ return format_out({"wim_port_mapping": data})
+
+ @route('GET', '/<tenant_id>/wims/<wim_id>/port_mapping')
+ def http_get_wim_port_mappings(self, tenant_id, wim_id):
+ """Get wim port mapping details"""
+ # TODO: tenant_id is never used, so it should be removed
+ data = self.engine.get_wim_port_mappings(wim_id)
+ return format_out({"wim_port_mapping": data})
+
+ @route('DELETE', '/<tenant_id>/wims/<wim_id>/port_mapping')
+ def http_delete_wim_port_mappings(self, tenant_id, wim_id):
+ """Clean wim port mapping"""
+ # TODO: tenant_id is never used, so it should be removed
+ data = self.engine.delete_wim_port_mappings(wim_id)
+ return format_out({"result": data})
diff --git a/osm_ro/wim/persistence.py b/osm_ro/wim/persistence.py
new file mode 100644
index 0000000..8a74d49
--- /dev/null
+++ b/osm_ro/wim/persistence.py
@@ -0,0 +1,1018 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+"""This module contains only logic related to managing records in a database
+which includes data format normalization, data format validation and etc.
+(It works as an extension to `nfvo_db.py` for the WIM feature)
+
+No domain logic/architectural concern should be present in this file.
+"""
+import json
+import logging
+from contextlib import contextmanager
+from hashlib import sha1
+from itertools import groupby
+from operator import itemgetter
+from sys import exc_info
+from threading import Lock
+from time import time
+from uuid import uuid1 as generate_uuid
+
+from six import reraise
+
+import yaml
+
+from ..utils import (
+ check_valid_uuid,
+ convert_float_timestamp2str,
+ expand_joined_fields,
+ filter_dict_keys,
+ filter_out_dict_keys,
+ merge_dicts,
+ remove_none_items
+)
+from .errors import (
+ DbBaseException,
+ InvalidParameters,
+ MultipleRecordsFound,
+ NoRecordFound,
+ UndefinedUuidOrName,
+ UndefinedWanMappingType,
+ UnexpectedDatabaseError,
+ WimAccountOverwrite,
+ WimAndTenantAlreadyAttached
+)
+
+_WIM = 'wims AS wim '
+
+_WIM_JOIN = (
+ _WIM +
+ ' JOIN wim_nfvo_tenants AS association '
+ ' ON association.wim_id=wim.uuid '
+ ' JOIN nfvo_tenants AS nfvo_tenant '
+ ' ON association.nfvo_tenant_id=nfvo_tenant.uuid '
+ ' JOIN wim_accounts AS wim_account '
+ ' ON association.wim_account_id=wim_account.uuid '
+)
+
+_WIM_ACCOUNT_JOIN = (
+ 'wim_accounts AS wim_account '
+ ' JOIN wim_nfvo_tenants AS association '
+ ' ON association.wim_account_id=wim_account.uuid '
+ ' JOIN wims AS wim '
+ ' ON association.wim_id=wim.uuid '
+ ' JOIN nfvo_tenants AS nfvo_tenant '
+ ' ON association.nfvo_tenant_id=nfvo_tenant.uuid '
+)
+
+_DATACENTER_JOIN = (
+ 'datacenters AS datacenter '
+ ' JOIN tenants_datacenters AS association '
+ ' ON association.datacenter_id=datacenter.uuid '
+ ' JOIN datacenter_tenants as datacenter_account '
+ ' ON association.datacenter_tenant_id=datacenter_account.uuid '
+ ' JOIN nfvo_tenants AS nfvo_tenant '
+ ' ON association.nfvo_tenant_id=nfvo_tenant.uuid '
+)
+
+_PORT_MAPPING = 'wim_port_mappings as wim_port_mapping '
+
+_PORT_MAPPING_JOIN_WIM = (
+ ' JOIN wims as wim '
+ ' ON wim_port_mapping.wim_id=wim.uuid '
+)
+
+_PORT_MAPPING_JOIN_DATACENTER = (
+ ' JOIN datacenters as datacenter '
+ ' ON wim_port_mapping.datacenter_id=datacenter.uuid '
+)
+
+_WIM_SELECT = [
+ 'wim.{0} as {0}'.format(_field)
+ for _field in 'uuid name description wim_url type config '
+ 'created_at modified_at'.split()
+]
+
+_WIM_ACCOUNT_SELECT = 'uuid name user password config'.split()
+
+_PORT_MAPPING_SELECT = ('wim_port_mapping.*', )
+
+_CONFIDENTIAL_FIELDS = ('password', 'passwd')
+
+_SERIALIZED_FIELDS = ('config', 'vim_info', 'wim_info', 'conn_info', 'extra',
+ 'wan_service_mapping_info')
+
+UNIQUE_PORT_MAPPING_INFO_FIELDS = {
+ 'dpid-port': ('wan_switch_dpid', 'wan_switch_port')
+}
+"""Fields that should be unique for each port mapping that relies on
+wan_service_mapping_info.
+
+For example, for port mappings of type 'dpid-port', each combination of
+wan_switch_dpid and wan_switch_port should be unique (the same switch cannot
+be connected to two different places using the same port)
+"""
+
+
+class WimPersistence(object):
+ """High level interactions with the WIM tables in the database"""
+
+ def __init__(self, db, logger=None, lock=None):
+ self.db = db
+ self.logger = logger or logging.getLogger('openmano.wim.persistence')
+ self.lock = lock or Lock()
+
+ def query(self,
+ FROM=None,
+ SELECT=None,
+ WHERE=None,
+ ORDER_BY=None,
+ LIMIT=None,
+ OFFSET=None,
+ error_if_none=True,
+ error_if_multiple=False,
+ postprocess=None,
+ hide=_CONFIDENTIAL_FIELDS,
+ **kwargs):
+ """Retrieve records from the database.
+
+ Keyword Arguments:
+ SELECT, FROM, WHERE, LIMIT, ORDER_BY: used to compose the SQL
+ query. See ``nfvo_db.get_rows``.
+ OFFSET: only valid when used togheter with LIMIT.
+ Ignore the OFFSET first results of the query.
+ error_if_none: by default an error is raised if no record is
+ found. With this option it is possible to disable this error.
+ error_if_multiple: by default no error is raised if more then one
+ record is found.
+ With this option it is possible to enable this error.
+ postprocess: function applied to every retrieved record.
+ This function receives a dict as input and must return it
+ after modifications. Moreover this function should accept a
+ second optional parameter ``hide`` indicating
+ the confidential fiels to be obfuscated.
+ By default a minimal postprocessing function is applied,
+ obfuscating confidential fields and converting timestamps.
+ hide: option proxied to postprocess
+
+ All the remaining keyword arguments will be assumed to be ``name``s or
+ ``uuid``s to compose the WHERE statement, according to their format.
+ If the value corresponds to an array, the first element will determine
+ if it is an name or UUID.
+
+ For example:
+ - ``wim="abcdef"``` will be turned into ``wim.name="abcdef"``,
+ - ``datacenter="5286a274-8a1b-4b8d-a667-9c94261ad855"``
+ will be turned into
+ ``datacenter.uuid="5286a274-8a1b-4b8d-a667-9c94261ad855"``.
+ - ``wim=["5286a274-8a1b-4b8d-a667-9c94261ad855", ...]``
+ will be turned into
+ ``wim.uuid=["5286a274-8a1b-4b8d-a667-9c94261ad855", ...]``
+
+ Raises:
+ NoRecordFound: if the query result set is empty
+ DbBaseException: errors occuring during the execution of the query.
+ """
+ # Defaults:
+ postprocess = postprocess or _postprocess_record
+ WHERE = WHERE or {}
+
+ # Find remaining keywords by name or uuid
+ WHERE.update(_compose_where_from_uuids_or_names(**kwargs))
+ WHERE = WHERE or None
+ # ^ If the where statement is empty, it is better to leave it as None,
+ # so it can be filtered out at a later stage
+ LIMIT = ('{:d},{:d}'.format(OFFSET, LIMIT)
+ if LIMIT and OFFSET else LIMIT)
+
+ query = remove_none_items({
+ 'SELECT': SELECT, 'FROM': FROM, 'WHERE': WHERE,
+ 'LIMIT': LIMIT, 'ORDER_BY': ORDER_BY})
+
+ with self.lock:
+ records = self.db.get_rows(**query)
+
+ table = FROM.split()[0]
+ if error_if_none and not records:
+ raise NoRecordFound(WHERE, table)
+
+ if error_if_multiple and len(records) > 1:
+ self.logger.error('Multiple records '
+ 'FROM %s WHERE %s:\n\n%s\n\n',
+ FROM, WHERE, json.dumps(records, indent=4))
+ raise MultipleRecordsFound(WHERE, table)
+
+ return [
+ expand_joined_fields(postprocess(record, hide))
+ for record in records
+ ]
+
+ def query_one(self, *args, **kwargs):
+ """Similar to ``query``, but ensuring just one result.
+ ``error_if_multiple`` is enabled by default.
+ """
+ kwargs.setdefault('error_if_multiple', True)
+ records = self.query(*args, **kwargs)
+ return records[0] if records else None
+
+ def get_by_uuid(self, table, uuid, **kwargs):
+ """Retrieve one record from the database based on its uuid
+
+ Arguments:
+ table (str): table name (to be used in SQL's FROM statement).
+ uuid (str): unique identifier for record.
+
+ For additional keyword arguments and exceptions see :obj:`~.query`
+ (``error_if_multiple`` is enabled by default).
+ """
+ if uuid is None:
+ raise UndefinedUuidOrName(table)
+ return self.query_one(table, WHERE={'uuid': uuid}, **kwargs)
+
+ def get_by_name_or_uuid(self, table, uuid_or_name, **kwargs):
+ """Retrieve a record from the database based on a value that can be its
+ uuid or name.
+
+ Arguments:
+ table (str): table name (to be used in SQL's FROM statement).
+ uuid_or_name (str): this value can correspond to either uuid or
+ name
+ For additional keyword arguments and exceptions see :obj:`~.query`
+ (``error_if_multiple`` is enabled by default).
+ """
+ if uuid_or_name is None:
+ raise UndefinedUuidOrName(table)
+
+ key = 'uuid' if check_valid_uuid(uuid_or_name) else 'name'
+ return self.query_one(table, WHERE={key: uuid_or_name}, **kwargs)
+
+ def get_wims(self, uuid_or_name=None, tenant=None, **kwargs):
+ """Retrieve information about one or more WIMs stored in the database
+
+ Arguments:
+ uuid_or_name (str): uuid or name for WIM
+ tenant (str): [optional] uuid or name for NFVO tenant
+
+ See :obj:`~.query` for additional keyword arguments.
+ """
+ kwargs.update(wim=uuid_or_name, tenant=tenant)
+ from_ = _WIM_JOIN if tenant else _WIM
+ select_ = _WIM_SELECT[:] + (['wim_account.*'] if tenant else [])
+
+ kwargs.setdefault('SELECT', select_)
+ return self.query(from_, **kwargs)
+
+ def get_wim(self, wim, tenant=None, **kwargs):
+ """Similar to ``get_wims`` but ensure only one result is returned"""
+ kwargs.setdefault('error_if_multiple', True)
+ return self.get_wims(wim, tenant)[0]
+
+ def create_wim(self, wim_descriptor):
+ """Create a new wim record inside the database and returns its uuid
+
+ Arguments:
+ wim_descriptor (dict): properties of the record
+ (usually each field corresponds to a database column, but extra
+ information can be offloaded to another table or serialized as
+ JSON/YAML)
+ Returns:
+ str: UUID of the created WIM
+ """
+ if "config" in wim_descriptor:
+ wim_descriptor["config"] = _serialize(wim_descriptor["config"])
+
+ with self.lock:
+ return self.db.new_row(
+ "wims", wim_descriptor, add_uuid=True, confidential_data=True)
+
+ def update_wim(self, uuid_or_name, wim_descriptor):
+ """Change an existing WIM record on the database"""
+ # obtain data, check that only one exist
+ wim = self.get_by_name_or_uuid('wims', uuid_or_name)
+
+ # edit data
+ wim_id = wim['uuid']
+ where = {'uuid': wim['uuid']}
+
+ # unserialize config, edit and serialize it again
+ if wim_descriptor.get('config'):
+ new_config_dict = wim_descriptor["config"]
+ config_dict = remove_none_items(merge_dicts(
+ wim.get('config') or {}, new_config_dict))
+ wim_descriptor['config'] = (
+ _serialize(config_dict) if config_dict else None)
+
+ with self.lock:
+ self.db.update_rows('wims', wim_descriptor, where)
+
+ return wim_id
+
+ def delete_wim(self, wim):
+ # get nfvo_tenant info
+ wim = self.get_by_name_or_uuid('wims', wim)
+
+ with self.lock:
+ self.db.delete_row_by_id('wims', wim['uuid'])
+
+ return wim['uuid'] + ' ' + wim['name']
+
+ def get_wim_accounts_by(self, wim=None, tenant=None, uuid=None, **kwargs):
+ """Retrieve WIM account information from the database together
+ with the related records (wim, nfvo_tenant and wim_nfvo_tenant)
+
+ Arguments:
+ wim (str): uuid or name for WIM
+ tenant (str): [optional] uuid or name for NFVO tenant
+
+ See :obj:`~.query` for additional keyword arguments.
+ """
+ kwargs.update(wim=wim, tenant=tenant)
+ kwargs.setdefault('postprocess', _postprocess_wim_account)
+ if uuid:
+ kwargs.setdefault('WHERE', {'wim_account.uuid': uuid})
+ return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs)
+
+ def get_wim_account_by(self, wim=None, tenant=None, **kwargs):
+ """Similar to ``get_wim_accounts_by``, but ensuring just one result"""
+ kwargs.setdefault('error_if_multiple', True)
+ return self.get_wim_accounts_by(wim, tenant, **kwargs)[0]
+
+ def get_wim_accounts(self, **kwargs):
+ """Retrieve all the accounts from the database"""
+ kwargs.setdefault('postprocess', _postprocess_wim_account)
+ return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs)
+
+ def get_wim_account(self, uuid_or_name, **kwargs):
+ """Retrieve WIM Account record by UUID or name,
+ See :obj:`get_by_name_or_uuid` for keyword arguments.
+ """
+ kwargs.setdefault('postprocess', _postprocess_wim_account)
+ kwargs.setdefault('SELECT', _WIM_ACCOUNT_SELECT)
+ return self.get_by_name_or_uuid('wim_accounts', uuid_or_name, **kwargs)
+
+ @contextmanager
+ def _associate(self, wim_id, nfvo_tenant_id):
+ """Auxiliary method for ``create_wim_account``
+
+ This method just create a row in the association table
+ ``wim_nfvo_tenants``
+ """
+ try:
+ with self.lock:
+ yield
+ except DbBaseException as db_exception:
+ error_msg = str(db_exception)
+ if all([msg in error_msg
+ for msg in ("already in use", "'wim_nfvo_tenant'")]):
+ ex = WimAndTenantAlreadyAttached(wim_id, nfvo_tenant_id)
+ reraise(ex.__class__, ex, exc_info()[2])
+
+ raise
+
+ def create_wim_account(self, wim, tenant, properties):
+ """Associate a wim to a tenant using the ``wim_nfvo_tenants`` table
+ and create a ``wim_account`` to store credentials and configurations.
+
+ For the sake of simplification, we assume that each NFVO tenant can be
+ attached to a WIM using only one WIM account. This is automatically
+ guaranteed via database constraints.
+ For corner cases, the same WIM can be registered twice using another
+ name.
+
+ Arguments:
+ wim (str): name or uuid of the WIM related to the account being
+ created
+ tenant (str): name or uuid of the nfvo tenant to which the account
+ will be created
+ properties (dict): properties of the account
+ (eg. user, password, ...)
+ """
+ wim_id = self.get_by_name_or_uuid('wims', wim, SELECT=['uuid'])['uuid']
+ tenant = self.get_by_name_or_uuid('nfvo_tenants', tenant,
+ SELECT=['uuid', 'name'])
+ account = properties.setdefault('name', tenant['name'])
+
+ wim_account = self.query_one('wim_accounts',
+ WHERE={'wim_id': wim_id, 'name': account},
+ error_if_none=False)
+
+ transaction = []
+ used_uuids = []
+
+ if wim_account is None:
+ # If a row for the wim account doesn't exist yet, we need to
+ # create one, otherwise we can just re-use it.
+ account_id = str(generate_uuid())
+ used_uuids.append(account_id)
+ row = merge_dicts(properties, wim_id=wim_id, uuid=account_id)
+ transaction.append({'wim_accounts': _preprocess_wim_account(row)})
+ else:
+ account_id = wim_account['uuid']
+ properties.pop('config', None) # Config is too complex to compare
+ diff = {k: v for k, v in properties.items() if v != wim_account[k]}
+ if diff:
+ tip = 'Edit the account first, and then attach it to a tenant'
+ raise WimAccountOverwrite(wim_account, diff, tip)
+
+ transaction.append({
+ 'wim_nfvo_tenants': {'nfvo_tenant_id': tenant['uuid'],
+ 'wim_id': wim_id,
+ 'wim_account_id': account_id}})
+
+ with self._associate(wim_id, tenant['uuid']):
+ self.db.new_rows(transaction, used_uuids, confidential_data=True)
+
+ return account_id
+
+ def update_wim_account(self, uuid, properties, hide=_CONFIDENTIAL_FIELDS):
+ """Update WIM account record by overwriting fields with new values
+
+ Specially for the field ``config`` this means that a new dict will be
+ merged to the existing one.
+
+ Attributes:
+ uuid (str): UUID for the WIM account
+ properties (dict): fields that should be overwritten
+
+ Returns:
+ Updated wim_account
+ """
+ wim_account = self.get_by_uuid('wim_accounts', uuid)
+ safe_fields = 'user password name created'.split()
+ updates = _preprocess_wim_account(
+ merge_dicts(wim_account, filter_dict_keys(properties, safe_fields))
+ )
+
+ if properties.get('config'):
+ old_config = wim_account.get('config') or {}
+ new_config = merge_dicts(old_config, properties['config'])
+ updates['config'] = _serialize(new_config)
+
+ with self.lock:
+ num_changes = self.db.update_rows(
+ 'wim_accounts', UPDATE=updates,
+ WHERE={'uuid': wim_account['uuid']})
+
+ if num_changes is None:
+ raise UnexpectedDatabaseError('Impossible to update wim_account '
+ '{name}:{uuid}'.format(*wim_account))
+
+ return self.get_wim_account(wim_account['uuid'], hide=hide)
+
+ def delete_wim_account(self, uuid):
+ """Remove WIM account record from the database"""
+ # Since we have foreign keys configured with ON CASCADE, we can rely
+ # on the database engine to guarantee consistency, deleting the
+ # dependant records
+ with self.lock:
+ return self.db.delete_row_by_id('wim_accounts', uuid)
+
+ def get_datacenters_by(self, datacenter=None, tenant=None, **kwargs):
+ """Retrieve datacenter information from the database together
+ with the related records (nfvo_tenant)
+
+ Arguments:
+ datacenter (str): uuid or name for datacenter
+ tenant (str): [optional] uuid or name for NFVO tenant
+
+ See :obj:`~.query` for additional keyword arguments.
+ """
+ kwargs.update(datacenter=datacenter, tenant=tenant)
+ return self.query(_DATACENTER_JOIN, **kwargs)
+
+ def get_datacenter_by(self, datacenter=None, tenant=None, **kwargs):
+ """Similar to ``get_datacenters_by``, but ensuring just one result"""
+ kwargs.setdefault('error_if_multiple', True)
+ return self.get_datacenters_by(datacenter, tenant, **kwargs)[0]
+
+ def _create_single_port_mapping(self, properties):
+ info = properties.setdefault('wan_service_mapping_info', {})
+ endpoint_id = properties.get('wan_service_endpoint_id')
+
+ if info.get('mapping_type') and not endpoint_id:
+ properties['wan_service_endpoint_id'] = (
+ self._generate_port_mapping_id(info))
+
+ properties['wan_service_mapping_info'] = _serialize(info)
+
+ try:
+ with self.lock:
+ self.db.new_row('wim_port_mappings', properties,
+ add_uuid=False, confidential_data=True)
+ except DbBaseException as old_exception:
+ self.logger.exception(old_exception)
+ ex = InvalidParameters(
+ "The mapping must contain the "
+ "'pop_switch_dpid', 'pop_switch_port', and "
+ "wan_service_mapping_info: "
+ "('wan_switch_dpid' and 'wan_switch_port') or "
+ "'wan_service_endpoint_id}'")
+ reraise(ex.__class__, ex, exc_info()[2])
+
+ return properties
+
+ def create_wim_port_mappings(self, wim, port_mappings, tenant=None):
+ if not isinstance(wim, dict):
+ wim = self.get_by_name_or_uuid('wims', wim)
+
+ for port_mapping in port_mappings:
+ port_mapping['wim_name'] = wim['name']
+ datacenter = self.get_datacenter_by(
+ port_mapping['datacenter_name'], tenant)
+ for pop_wan_port_mapping in port_mapping['pop_wan_mappings']:
+ element = merge_dicts(pop_wan_port_mapping, {
+ 'wim_id': wim['uuid'],
+ 'datacenter_id': datacenter['uuid']})
+ self._create_single_port_mapping(element)
+
+ return port_mappings
+
+ def _filter_port_mappings_by_tenant(self, mappings, tenant):
+ """Make sure all the datacenters and wims listed in the port mapping
+ belong to an specific tenant
+ """
+
+ # NOTE: Theoretically this could be done at SQL level, but given the
+ # number of tables involved (wim_port_mappings, wim_accounts,
+ # wims, wim_nfvo_tenants, datacenters, datacenter_tenants,
+ # tenants_datacents and nfvo_tenants), it would result in a
+ # extremely complex query. Moreover, the predicate can vary:
+ # for `get_wim_port_mappings` we can have any combination of
+ # (wim, datacenter, tenant), not all of them having the 3 values
+ # so we have combinatorial trouble to write the 'FROM' statement.
+
+ kwargs = {'tenant': tenant, 'error_if_none': False}
+ # Cache results to speedup things
+ datacenters = {}
+ wims = {}
+
+ def _get_datacenter(uuid):
+ return (
+ datacenters.get(uuid) or
+ datacenters.setdefault(
+ uuid, self.get_datacenters_by(uuid, **kwargs)))
+
+ def _get_wims(uuid):
+ return (wims.get(uuid) or
+ wims.setdefault(uuid, self.get_wims(uuid, **kwargs)))
+
+ return [
+ mapping
+ for mapping in mappings
+ if (_get_datacenter(mapping['datacenter_id']) and
+ _get_wims(mapping['wim_id']))
+ ]
+
+ def get_wim_port_mappings(self, wim=None, datacenter=None, tenant=None,
+ **kwargs):
+ """List all the port mappings, optionally filtering by wim, datacenter
+ AND/OR tenant
+ """
+ from_ = [_PORT_MAPPING,
+ _PORT_MAPPING_JOIN_WIM if wim else '',
+ _PORT_MAPPING_JOIN_DATACENTER if datacenter else '']
+
+ criteria = ('wim_id', 'datacenter_id')
+ kwargs.setdefault('error_if_none', False)
+ mappings = self.query(
+ ' '.join(from_),
+ SELECT=_PORT_MAPPING_SELECT,
+ ORDER_BY=['wim_port_mapping.{}'.format(c) for c in criteria],
+ wim=wim, datacenter=datacenter,
+ postprocess=_postprocess_wim_port_mapping,
+ **kwargs)
+
+ if tenant:
+ mappings = self._filter_port_mappings_by_tenant(mappings, tenant)
+
+ # We don't have to sort, since we have used 'ORDER_BY'
+ grouped_mappings = groupby(mappings, key=itemgetter(*criteria))
+
+ return [
+ {'wim_id': key[0],
+ 'datacenter_id': key[1],
+ 'wan_pop_port_mappings': [
+ filter_out_dict_keys(mapping, (
+ 'id', 'wim_id', 'datacenter_id',
+ 'created_at', 'modified_at'))
+ for mapping in group]}
+ for key, group in grouped_mappings
+ ]
+
+ def delete_wim_port_mappings(self, wim_id):
+ with self.lock:
+ self.db.delete_row(FROM='wim_port_mappings',
+ WHERE={"wim_id": wim_id})
+ return "port mapping for wim {} deleted.".format(wim_id)
+
+ def update_wim_port_mapping(self, id, properties):
+ original = self.query_one('wim_port_mappings', WHERE={'id': id})
+
+ mapping_info = remove_none_items(merge_dicts(
+ original.get('wan_service_mapping_info') or {},
+ properties.get('wan_service_mapping_info') or {}))
+
+ updates = preprocess_record(
+ merge_dicts(original, remove_none_items(properties),
+ wan_service_mapping_info=mapping_info))
+
+ with self.lock:
+ num_changes = self.db.update_rows(
+ 'wim_port_mappings', UPDATE=updates, WHERE={'id': id})
+
+ if num_changes is None:
+ raise UnexpectedDatabaseError(
+ 'Impossible to update wim_port_mappings %s:\n%s\n',
+ id, _serialize(properties))
+
+ return num_changes
+
+ def get_actions_in_groups(self, wim_account_id,
+ item_types=('instance_wim_nets',),
+ group_offset=0, group_limit=150):
+ """Retrieve actions from the database in groups.
+ Each group contains all the actions that have the same ``item`` type
+ and ``item_id``.
+
+ Arguments:
+ wim_account_id: restrict the search to actions to be performed
+ using the same account
+ item_types (list): [optional] filter the actions to the given
+ item types
+ group_limit (int): maximum number of groups returned by the
+ function
+ group_offset (int): skip the N first groups. Used together with
+ group_limit for pagination purposes.
+
+ Returns:
+ List of groups, where each group is a tuple ``(key, actions)``.
+ In turn, ``key`` is a tuple containing the values of
+ ``(item, item_id)`` used to create the group and ``actions`` is a
+ list of ``vim_wim_actions`` records (dicts).
+ """
+
+ type_options = set(
+ '"{}"'.format(self.db.escape_string(t)) for t in item_types)
+
+ items = ('SELECT DISTINCT a.item, a.item_id, a.wim_account_id '
+ 'FROM vim_wim_actions AS a '
+ 'WHERE a.wim_account_id="{}" AND a.item IN ({}) '
+ 'ORDER BY a.item, a.item_id '
+ 'LIMIT {:d},{:d}').format(
+ self.safe_str(wim_account_id),
+ ','.join(type_options),
+ group_offset, group_limit
+ )
+
+ join = 'vim_wim_actions NATURAL JOIN ({}) AS items'.format(items)
+ with self.lock:
+ db_results = self.db.get_rows(
+ FROM=join, ORDER_BY=('item', 'item_id', 'created_at'))
+
+ results = (_postprocess_action(r) for r in db_results)
+ criteria = itemgetter('item', 'item_id')
+ return [(k, list(g)) for k, g in groupby(results, key=criteria)]
+
+ def update_action(self, instance_action_id, task_index, properties):
+ condition = {'instance_action_id': instance_action_id,
+ 'task_index': task_index}
+ action = self.query_one('vim_wim_actions', WHERE=condition)
+
+ extra = remove_none_items(merge_dicts(
+ action.get('extra') or {},
+ properties.get('extra') or {}))
+
+ updates = preprocess_record(
+ merge_dicts(action, properties, extra=extra))
+
+ with self.lock:
+ num_changes = self.db.update_rows('vim_wim_actions',
+ UPDATE=updates, WHERE=condition)
+
+ if num_changes is None:
+ raise UnexpectedDatabaseError(
+ 'Impossible to update vim_wim_actions '
+ '{instance_action_id}[{task_index}]'.format(*action))
+
+ return num_changes
+
+ def get_wan_links(self, uuid=None, **kwargs):
+ """Retrieve WAN link records from the database
+
+ Keyword Arguments:
+ uuid, instance_scenario_id, sce_net_id, wim_id, wim_account_id:
+ attributes that can be used at the WHERE clause
+ """
+ kwargs.setdefault('uuid', uuid)
+ kwargs.setdefault('error_if_none', False)
+
+ criteria_fields = ('uuid', 'instance_scenario_id', 'sce_net_id',
+ 'wim_id', 'wim_account_id')
+ criteria = remove_none_items(filter_dict_keys(kwargs, criteria_fields))
+ kwargs = filter_out_dict_keys(kwargs, criteria_fields)
+
+ return self.query('instance_wim_nets', WHERE=criteria, **kwargs)
+
+ def update_wan_link(self, uuid, properties):
+ wan_link = self.get_by_uuid('instance_wim_nets', uuid)
+
+ wim_info = remove_none_items(merge_dicts(
+ wan_link.get('wim_info') or {},
+ properties.get('wim_info') or {}))
+
+ updates = preprocess_record(
+ merge_dicts(wan_link, properties, wim_info=wim_info))
+
+ self.logger.debug({'UPDATE': updates})
+ with self.lock:
+ num_changes = self.db.update_rows(
+ 'instance_wim_nets', UPDATE=updates,
+ WHERE={'uuid': wan_link['uuid']})
+
+ if num_changes is None:
+ raise UnexpectedDatabaseError(
+ 'Impossible to update instance_wim_nets ' + wan_link['uuid'])
+
+ return num_changes
+
+ def get_instance_nets(self, instance_scenario_id, sce_net_id, **kwargs):
+ """Retrieve all the instance nets related to the same instance_scenario
+ and scenario network
+ """
+ return self.query(
+ 'instance_nets',
+ WHERE={'instance_scenario_id': instance_scenario_id,
+ 'sce_net_id': sce_net_id},
+ ORDER_BY=kwargs.pop(
+ 'ORDER_BY', ('instance_scenario_id', 'sce_net_id')),
+ **kwargs)
+
+ def update_instance_action_counters(self, uuid, failed=None, done=None):
+ """Atomically increment/decrement number_done and number_failed fields
+ in the instance action table
+ """
+ changes = remove_none_items({
+ 'number_failed': failed and {'INCREMENT': failed},
+ 'number_done': done and {'INCREMENT': done}
+ })
+
+ if not changes:
+ return 0
+
+ with self.lock:
+ return self.db.update_rows('instance_actions',
+ WHERE={'uuid': uuid}, UPDATE=changes)
+
+ def get_only_vm_with_external_net(self, instance_net_id, **kwargs):
+ """Return an instance VM if that is the only VM connected to an
+ external network identified by instance_net_id
+ """
+ counting = ('SELECT DISTINCT instance_net_id '
+ 'FROM instance_interfaces '
+ 'WHERE instance_net_id="{}" AND type="external" '
+ 'GROUP BY instance_net_id '
+ 'HAVING COUNT(*)=1').format(self.safe_str(instance_net_id))
+
+ vm_item = ('SELECT DISTINCT instance_vm_id '
+ 'FROM instance_interfaces NATURAL JOIN ({}) AS a'
+ .format(counting))
+
+ return self.query_one(
+ 'instance_vms JOIN ({}) as instance_interface '
+ 'ON instance_vms.uuid=instance_interface.instance_vm_id'
+ .format(vm_item), **kwargs)
+
+ def safe_str(self, string):
+ """Return a SQL safe string"""
+ return self.db.escape_string(string)
+
+ def _generate_port_mapping_id(self, mapping_info):
+ """Given a port mapping represented by a dict with a 'type' field,
+ generate a unique string, in a injective way.
+ """
+ mapping_info = mapping_info.copy() # Avoid mutating original object
+ mapping_type = mapping_info.pop('mapping_type', None)
+ if not mapping_type:
+ raise UndefinedWanMappingType(mapping_info)
+
+ unique_fields = UNIQUE_PORT_MAPPING_INFO_FIELDS.get(mapping_type)
+
+ if unique_fields:
+ mapping_info = filter_dict_keys(mapping_info, unique_fields)
+ else:
+ self.logger.warning('Unique fields for WIM port mapping of type '
+ '%s not defined. Please add a list of fields '
+ 'which combination should be unique in '
+ 'UNIQUE_PORT_MAPPING_INFO_FIELDS '
+ '(`wim/persistency.py) ', mapping_type)
+
+ repeatable_repr = json.dumps(mapping_info, encoding='utf-8',
+ sort_keys=True, indent=False)
+
+ return ':'.join([mapping_type, _str2id(repeatable_repr)])
+
+
+def _serialize(value):
+ """Serialize an arbitrary value in a consistent way,
+ so it can be stored in a database inside a text field
+ """
+ return yaml.safe_dump(value, default_flow_style=True, width=256)
+
+
+def _unserialize(text):
+ """Unserialize text representation into an arbitrary value,
+ so it can be loaded from the database
+ """
+ return yaml.safe_load(text)
+
+
+def preprocess_record(record):
+ """Small transformations to be applied to the data that cames from the
+ user before writing it to the database. By default, filter out timestamps,
+ and serialize the ``config`` field.
+ """
+ automatic_fields = ['created_at', 'modified_at']
+ record = serialize_fields(filter_out_dict_keys(record, automatic_fields))
+
+ return record
+
+
+def _preprocess_wim_account(wim_account):
+ """Do the default preprocessing and convert the 'created' field from
+ boolean to string
+ """
+ wim_account = preprocess_record(wim_account)
+
+ created = wim_account.get('created')
+ wim_account['created'] = (
+ 'true' if created is True or created == 'true' else 'false')
+
+ return wim_account
+
+
+def _postprocess_record(record, hide=_CONFIDENTIAL_FIELDS):
+ """By default, hide passwords fields, unserialize ``config`` fields, and
+ convert float timestamps to strings
+ """
+ record = hide_confidential_fields(record, hide)
+ record = unserialize_fields(record, hide)
+
+ convert_float_timestamp2str(record)
+
+ return record
+
+
+def _postprocess_action(action):
+ if action.get('extra'):
+ action['extra'] = _unserialize(action['extra'])
+
+ return action
+
+
+def _postprocess_wim_account(wim_account, hide=_CONFIDENTIAL_FIELDS):
+ """Do the default postprocessing and convert the 'created' field from
+ string to boolean
+ """
+ # Fix fields from join
+ for field in ('type', 'description', 'wim_url'):
+ if field in wim_account:
+ wim_account['wim.'+field] = wim_account.pop(field)
+
+ for field in ('id', 'nfvo_tenant_id', 'wim_account_id'):
+ if field in wim_account:
+ wim_account['association.'+field] = wim_account.pop(field)
+
+ wim_account = _postprocess_record(wim_account, hide)
+
+ created = wim_account.get('created')
+ wim_account['created'] = (created is True or created == 'true')
+
+ return wim_account
+
+
+def _postprocess_wim_port_mapping(mapping, hide=_CONFIDENTIAL_FIELDS):
+ mapping = _postprocess_record(mapping, hide=hide)
+ mapping_info = mapping.get('wan_service_mapping_info', None) or {}
+ mapping['wan_service_mapping_info'] = mapping_info
+ return mapping
+
+
+def hide_confidential_fields(record, fields=_CONFIDENTIAL_FIELDS):
+ """Obfuscate confidential fields from the input dict.
+
+ Note:
+ This function performs a SHALLOW operation.
+ """
+ if not(isinstance(record, dict) and fields):
+ return record
+
+ keys = record.iterkeys()
+ keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f))
+
+ return merge_dicts(record, {k: '********' for k in keys if record[k]})
+
+
+def unserialize_fields(record, hide=_CONFIDENTIAL_FIELDS,
+ fields=_SERIALIZED_FIELDS):
+ """Unserialize fields that where stored in the database as a serialized
+ YAML (or JSON)
+ """
+ keys = record.iterkeys()
+ keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f))
+
+ return merge_dicts(record, {
+ key: hide_confidential_fields(_unserialize(record[key]), hide)
+ for key in keys if record[key]
+ })
+
+
+def serialize_fields(record, fields=_SERIALIZED_FIELDS):
+ """Serialize fields to be stored in the database as YAML"""
+ keys = record.iterkeys()
+ keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f))
+
+ return merge_dicts(record, {
+ key: _serialize(record[key])
+ for key in keys if record[key] is not None
+ })
+
+
+def _decide_name_or_uuid(value):
+ reference = value
+
+ if isinstance(value, (list, tuple)):
+ reference = value[0] if value else ''
+
+ return 'uuid' if check_valid_uuid(reference) else 'name'
+
+
+def _compose_where_from_uuids_or_names(**conditions):
+ """Create a dict containing the right conditions to be used in a database
+ query.
+
+ This function chooses between ``names`` and ``uuid`` fields based on the
+ format of the passed string.
+ If a list is passed, the first element of the list will be used to choose
+ the name of the field.
+ If a ``None`` value is passed, ``uuid`` is used.
+
+ Note that this function automatically translates ``tenant`` to
+ ``nfvo_tenant`` for the sake of brevity.
+
+ Example:
+ >>> _compose_where_from_uuids_or_names(
+ wim='abcdef',
+ tenant=['xyz123', 'def456']
+ datacenter='5286a274-8a1b-4b8d-a667-9c94261ad855')
+ {'wim.name': 'abcdef',
+ 'nfvo_tenant.name': ['xyz123', 'def456']
+ 'datacenter.uuid': '5286a274-8a1b-4b8d-a667-9c94261ad855'}
+ """
+ if 'tenant' in conditions:
+ conditions['nfvo_tenant'] = conditions.pop('tenant')
+
+ return {
+ '{}.{}'.format(kind, _decide_name_or_uuid(value)): value
+ for kind, value in conditions.items() if value
+ }
+
+
+def _str2id(text):
+ """Create an ID (following the UUID format) from a piece of arbitrary
+ text.
+
+ Different texts should generate different IDs, and the same text should
+ generate the same ID in a repeatable way.
+ """
+ return sha1(text).hexdigest()
diff --git a/osm_ro/wim/schemas.py b/osm_ro/wim/schemas.py
new file mode 100644
index 0000000..a040405
--- /dev/null
+++ b/osm_ro/wim/schemas.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+from ..openmano_schemas import (
+ description_schema,
+ name_schema,
+ nameshort_schema
+)
+
+# WIM -------------------------------------------------------------------------
+wim_types = ["tapi", "onos", "odl"]
+
+wim_schema_properties = {
+ "name": name_schema,
+ "description": description_schema,
+ "type": {
+ "type": "string",
+ "enum": ["tapi", "onos", "odl"]
+ },
+ "wim_url": description_schema,
+ "config": {"type": "object"}
+}
+
+wim_schema = {
+ "title": "wim information schema",
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "properties": {
+ "wim": {
+ "type": "object",
+ "properties": wim_schema_properties,
+ "required": ["name", "type", "wim_url"],
+ "additionalProperties": True
+ }
+ },
+ "required": ["wim"],
+ "additionalProperties": False
+}
+
+wim_edit_schema = {
+ "title": "wim edit information schema",
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "properties": {
+ "wim": {
+ "type": "object",
+ "properties": wim_schema_properties,
+ "additionalProperties": False
+ }
+ },
+ "required": ["wim"],
+ "additionalProperties": False
+}
+
+wim_account_schema = {
+ "title": "wim account information schema",
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "properties": {
+ "wim_account": {
+ "type": "object",
+ "properties": {
+ "name": name_schema,
+ "user": nameshort_schema,
+ "password": nameshort_schema,
+ "config": {"type": "object"}
+ },
+ "additionalProperties": True
+ }
+ },
+ "required": ["wim_account"],
+ "additionalProperties": False
+}
+
+dpid_type = {
+ "type": "string",
+ "pattern":
+ "^[0-9a-zA-Z]+(:[0-9a-zA-Z]+)*$"
+}
+
+port_type = {
+ "oneOf": [
+ {"type": "string",
+ "minLength": 1,
+ "maxLength": 5},
+ {"type": "integer",
+ "minimum": 1,
+ "maximum": 65534}
+ ]
+}
+
+wim_port_mapping_schema = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "wim mapping information schema",
+ "type": "object",
+ "properties": {
+ "wim_port_mapping": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "datacenter_name": nameshort_schema,
+ "pop_wan_mappings": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "pop_switch_dpid": dpid_type,
+ "pop_switch_port": port_type,
+ "wan_service_endpoint_id": name_schema,
+ "wan_service_mapping_info": {
+ "type": "object",
+ "properties": {
+ "mapping_type": name_schema,
+ "wan_switch_dpid": dpid_type,
+ "wan_switch_port": port_type
+ },
+ "additionalProperties": True,
+ "required": ["mapping_type"]
+ }
+ },
+ "oneOf": [
+ {
+ "required": [
+ "pop_switch_dpid",
+ "pop_switch_port",
+ "wan_service_endpoint_id"
+ ]
+ },
+ {
+ "required": [
+ "pop_switch_dpid",
+ "pop_switch_port",
+ "wan_service_mapping_info"
+ ]
+ }
+ ]
+ }
+ }
+ },
+ "required": ["datacenter_name", "pop_wan_mappings"]
+ }
+ }
+ },
+ "required": ["wim_port_mapping"]
+}
diff --git a/osm_ro/wim/tests/__init__.py b/osm_ro/wim/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/osm_ro/wim/tests/__init__.py
diff --git a/osm_ro/wim/tests/fixtures.py b/osm_ro/wim/tests/fixtures.py
new file mode 100644
index 0000000..1b52e49
--- /dev/null
+++ b/osm_ro/wim/tests/fixtures.py
@@ -0,0 +1,307 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+# pylint: disable=W0621
+
+from __future__ import unicode_literals
+
+import json
+from time import time
+
+from six.moves import range
+
+from ...tests.db_helpers import uuid, sha1
+
+NUM_WIMS = 3
+NUM_TENANTS = 2
+NUM_DATACENTERS = 2
+
+
+# In the following functions, the identifiers should be simple integers
+
+
+def wim(identifier=0):
+ return {'name': 'wim%d' % identifier,
+ 'uuid': uuid('wim%d' % identifier),
+ 'wim_url': 'localhost',
+ 'type': 'tapi'}
+
+
+def tenant(identifier=0):
+ return {'name': 'tenant%d' % identifier,
+ 'uuid': uuid('tenant%d' % identifier)}
+
+
+def wim_account(wim, tenant):
+ return {'name': 'wim-account%d%d' % (tenant, wim),
+ 'uuid': uuid('wim-account%d%d' % (tenant, wim)),
+ 'user': 'user%d%d' % (tenant, wim),
+ 'password': 'password%d%d' % (tenant, wim),
+ 'wim_id': uuid('wim%d' % wim),
+ 'created': 'true'}
+
+
+def wim_tenant_association(wim, tenant):
+ return {'nfvo_tenant_id': uuid('tenant%d' % tenant),
+ 'wim_id': uuid('wim%d' % wim),
+ 'wim_account_id': uuid('wim-account%d%d' % (tenant, wim))}
+
+
+def wim_set(identifier=0, tenant=0):
+ """Records necessary to create a WIM and connect it to a tenant"""
+ return [
+ {'wims': [wim(identifier)]},
+ {'wim_accounts': [wim_account(identifier, tenant)]},
+ {'wim_nfvo_tenants': [wim_tenant_association(identifier, tenant)]}
+ ]
+
+
+def datacenter(identifier):
+ return {'uuid': uuid('dc%d' % identifier),
+ 'name': 'dc%d' % identifier,
+ 'type': 'openvim',
+ 'vim_url': 'localhost'}
+
+
+def datacenter_account(datacenter, tenant):
+ return {'name': 'dc-account%d%d' % (tenant, datacenter),
+ 'uuid': uuid('dc-account%d%d' % (tenant, datacenter)),
+ 'datacenter_id': uuid('dc%d' % datacenter),
+ 'created': 'true'}
+
+
+def datacenter_tenant_association(datacenter, tenant):
+ return {'nfvo_tenant_id': uuid('tenant%d' % tenant),
+ 'datacenter_id': uuid('dc%d' % datacenter),
+ 'datacenter_tenant_id':
+ uuid('dc-account%d%d' % (tenant, datacenter))}
+
+
+def datacenter_set(identifier, tenant):
+ """Records necessary to create a datacenter and connect it to a tenant"""
+ return [
+ {'datacenters': [datacenter(identifier)]},
+ {'datacenter_tenants': [datacenter_account(identifier, tenant)]},
+ {'tenants_datacenters': [
+ datacenter_tenant_association(identifier, tenant)
+ ]}
+ ]
+
+
+def wim_port_mapping(wim, datacenter,
+ pop_dpid='AA:AA:AA:AA:AA:AA:AA:AA', pop_port=0,
+ wan_dpid='BB:BB:BB:BB:BB:BB:BB:BB', wan_port=0):
+ mapping_info = {'mapping_type': 'dpid-port',
+ 'wan_switch_dpid': wan_dpid,
+ 'wan_switch_port': wan_port + datacenter + 1}
+ id_ = 'dpid-port|' + sha1(json.dumps(mapping_info, sort_keys=True))
+
+ return {'wim_id': uuid('wim%d' % wim),
+ 'datacenter_id': uuid('dc%d' % datacenter),
+ 'pop_switch_dpid': pop_dpid,
+ 'pop_switch_port': pop_port + wim + 1,
+ # ^ Datacenter router have one port managed by each WIM
+ 'wan_service_endpoint_id': id_,
+ # ^ WIM managed router have one port connected to each DC
+ 'wan_service_mapping_info': json.dumps(mapping_info)}
+
+
+def processed_port_mapping(wim, datacenter,
+ num_pairs=1,
+ pop_dpid='AA:AA:AA:AA:AA:AA:AA:AA',
+ wan_dpid='BB:BB:BB:BB:BB:BB:BB:BB'):
+ """Emulate the response of the Persistence class, where the records in the
+ data base are grouped by wim and datacenter
+ """
+ return {
+ 'wim_id': uuid('wim%d' % wim),
+ 'datacenter_id': uuid('dc%d' % datacenter),
+ 'wan_pop_port_mappings': [
+ {'pop_switch_dpid': pop_dpid,
+ 'pop_switch_port': wim + 1 + i,
+ 'wan_service_endpoint_id':
+ sha1('dpid-port|%s|%d' % (wan_dpid, datacenter + 1 + i)),
+ 'wan_service_mapping_info': {
+ 'mapping_type': 'dpid-port',
+ 'wan_switch_dpid': wan_dpid,
+ 'wan_switch_port': datacenter + 1 + i}}
+ for i in range(num_pairs)
+ ]
+ }
+
+
+def consistent_set(num_wims=NUM_WIMS, num_tenants=NUM_TENANTS,
+ num_datacenters=NUM_DATACENTERS):
+ return [
+ {'nfvo_tenants': [tenant(i) for i in range(num_tenants)]},
+ {'wims': [wim(j) for j in range(num_wims)]},
+ {'wim_accounts': [
+ wim_account(j, i)
+ for i in range(num_tenants)
+ for j in range(num_wims)
+ ]},
+ {'wim_nfvo_tenants': [
+ wim_tenant_association(j, i)
+ for i in range(num_tenants)
+ for j in range(num_wims)
+ ]},
+ {'datacenters': [
+ datacenter(k)
+ for k in range(num_datacenters)
+ ]},
+ {'datacenter_tenants': [
+ datacenter_account(k, i)
+ for i in range(num_tenants)
+ for k in range(num_datacenters)
+ ]},
+ {'tenants_datacenters': [
+ datacenter_tenant_association(k, i)
+ for i in range(num_tenants)
+ for k in range(num_datacenters)
+ ]},
+ {'wim_port_mappings': [
+ wim_port_mapping(j, k)
+ for j in range(num_wims)
+ for k in range(num_datacenters)
+ ]},
+ ]
+
+
+def instance_nets(num_datacenters=2, num_links=2):
+ """Example of multi-site deploy with N datacenters and M WAN links between
+ them (e.g M = 2 -> back and forth)
+ """
+ return [
+ {'uuid': uuid('net%d%d' % (k, l)),
+ 'datacenter_id': uuid('dc%d' % k),
+ 'datacenter_tenant_id': uuid('dc-account0%d' % k),
+ 'instance_scenario_id': uuid('nsr0'),
+ # ^ instance_scenario_id == NS Record id
+ 'sce_net_id': uuid('vld%d' % l),
+ # ^ scenario net id == VLD id
+ 'status': 'BUILD',
+ 'vim_net_id': None,
+ 'created': True}
+ for k in range(num_datacenters)
+ for l in range(num_links)
+ ]
+
+
+def wim_actions(action='CREATE', status='SCHEDULED',
+ action_id=None, instance=0,
+ wim=0, tenant=0, num_links=1):
+ """Create a list of actions for the WIM,
+
+ Arguments:
+ action: type of action (CREATE) by default
+ wim: WIM fixture index to create actions for
+ tenant: tenant fixture index to create actions for
+ num_links: number of WAN links to be established by each WIM
+ """
+
+ action_id = action_id or 'ACTION-{}'.format(time())
+
+ return [
+ {
+ 'action': action,
+ 'wim_internal_id': uuid('-wim-net%d%d%d' % (wim, instance, link)),
+ 'wim_account_id': uuid('wim-account%d%d' % (tenant, wim)),
+ 'instance_action_id': action_id,
+ 'item': 'instance_wim_nets',
+ 'item_id': uuid('wim-net%d%d%d' % (wim, instance, link)),
+ 'status': status,
+ 'task_index': link,
+ 'created_at': time(),
+ 'modified_at': time(),
+ 'extra': None
+ }
+ for link in range(num_links)
+ ]
+
+
+def instance_action(tenant=0, instance=0, action_id=None,
+ num_tasks=1, num_done=0, num_failed=0):
+ action_id = action_id or 'ACTION-{}'.format(time())
+
+ return {
+ 'uuid': action_id,
+ 'tenant_id': uuid('tenant%d' % tenant),
+ 'instance_id': uuid('nsr%d' % instance),
+ 'number_tasks': num_tasks,
+ 'number_done': num_done,
+ 'number_failed': num_failed,
+ }
+
+
+def instance_wim_nets(instance=0, wim=0, num_links=1,
+ status='SCHEDULED_CREATION'):
+ """Example of multi-site deploy with N wims and M WAN links between
+ them (e.g M = 2 -> back and forth)
+ VIM nets
+ """
+ return [
+ {'uuid': uuid('wim-net%d%d%d' % (wim, instance, l)),
+ 'wim_id': uuid('wim%d' % wim),
+ 'wim_account_id': uuid('wim-account%d' % wim),
+ 'wim_internal_id': uuid('-net%d%d' % (wim, l)),
+ 'instance_scenario_id': uuid('nsr%d' % instance),
+ # ^ instance_scenario_id == NS Record id
+ 'sce_net_id': uuid('vld%d' % l),
+ # ^ scenario net id == VLD id
+ 'status': status,
+ 'created': False}
+ for l in range(num_links)
+ ]
+
+
+def instance_vm(instance=0, vim_info=None):
+ vim_info = {'OS-EXT-SRV-ATTR:hypervisor_hostname': 'host%d' % instance}
+ return {
+ 'uuid': uuid('vm%d' % instance),
+ 'instance_vnf_id': uuid('vnf%d' % instance),
+ 'vm_id': uuid('vm%d' % instance),
+ 'vim_vm_id': uuid('vm%d' % instance),
+ 'status': 'ACTIVE',
+ 'vim_info': vim_info,
+ }
+
+
+def instance_interface(instance=0, interface=0, datacenter=0, link=0):
+ return {
+ 'uuid': uuid('interface%d%d' % (instance, interface)),
+ 'instance_vm_id': uuid('vm%d' % instance),
+ 'instance_net_id': uuid('net%d%d' % (datacenter, link)),
+ 'interface_id': uuid('iface%d' % interface),
+ 'type': 'external',
+ 'vlan': 3
+ }
diff --git a/osm_ro/wim/tests/test_actions.py b/osm_ro/wim/tests/test_actions.py
new file mode 100644
index 0000000..920182b
--- /dev/null
+++ b/osm_ro/wim/tests/test_actions.py
@@ -0,0 +1,366 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+# pylint: disable=E1101
+
+from __future__ import unicode_literals, print_function
+
+import json
+import unittest
+from time import time
+
+from mock import MagicMock, patch
+
+from . import fixtures as eg
+from ...tests.db_helpers import (
+ TestCaseWithDatabasePerTest,
+ disable_foreign_keys,
+ uuid,
+)
+from ..persistence import WimPersistence
+from ..wan_link_actions import WanLinkCreate, WanLinkDelete
+from ..wimconn import WimConnectorError
+
+
+class TestActionsWithDb(TestCaseWithDatabasePerTest):
+ def setUp(self):
+ super(TestActionsWithDb, self).setUp()
+ self.persist = WimPersistence(self.db)
+ self.connector = MagicMock()
+ self.ovim = MagicMock()
+
+
+class TestCreate(TestActionsWithDb):
+ @disable_foreign_keys
+ def test_process__instance_nets_on_build(self):
+ # Given we want 1 WAN link between 2 datacenters
+ # and the local network in each datacenter is still being built
+ wan_link = eg.instance_wim_nets()
+ instance_nets = eg.instance_nets(num_datacenters=2, num_links=1)
+ for net in instance_nets:
+ net['status'] = 'BUILD'
+ self.populate([{'instance_nets': instance_nets,
+ 'instance_wim_nets': wan_link}])
+
+ # When we try to process a CREATE action that refers to the same
+ # instance_scenario_id and sce_net_id
+ now = time()
+ action = WanLinkCreate(eg.wim_actions('CREATE')[0])
+ action.instance_scenario_id = instance_nets[0]['instance_scenario_id']
+ action.sce_net_id = instance_nets[0]['sce_net_id']
+ # -- ensure it is in the database for updates --> #
+ action_record = action.as_record()
+ action_record['extra'] = json.dumps(action_record['extra'])
+ self.populate([{'vim_wim_actions': action_record}])
+ # <-- #
+ action.process(self.connector, self.persist, self.ovim)
+
+ # Then the action should be defered
+ assert action.is_scheduled
+ self.assertEqual(action.extra['attempts'], 1)
+ self.assertGreater(action.extra['last_attempted_at'], now)
+
+ @disable_foreign_keys
+ def test_process__instance_nets_on_error(self):
+ # Given we want 1 WAN link between 2 datacenters
+ # and at least one local network is in a not good state (error, or
+ # being deleted)
+ instance_nets = eg.instance_nets(num_datacenters=2, num_links=1)
+ instance_nets[1]['status'] = 'SCHEDULED_DELETION'
+ wan_link = eg.instance_wim_nets()
+ self.populate([{'instance_nets': instance_nets,
+ 'instance_wim_nets': wan_link}])
+
+ # When we try to process a CREATE action that refers to the same
+ # instance_scenario_id and sce_net_id
+ action = WanLinkCreate(eg.wim_actions('CREATE')[0])
+ action.instance_scenario_id = instance_nets[0]['instance_scenario_id']
+ action.sce_net_id = instance_nets[0]['sce_net_id']
+ # -- ensure it is in the database for updates --> #
+ action_record = action.as_record()
+ action_record['extra'] = json.dumps(action_record['extra'])
+ self.populate([{'vim_wim_actions': action_record}])
+ # <-- #
+ action.process(self.connector, self.persist, self.ovim)
+
+ # Then the action should fail
+ assert action.is_failed
+ self.assertIn('issue with the local networks', action.error_msg)
+ self.assertIn('SCHEDULED_DELETION', action.error_msg)
+
+ def prepare_create__sdn(self):
+ db_state = [{'nfvo_tenants': eg.tenant()}] + eg.wim_set()
+
+ instance_nets = eg.instance_nets(num_datacenters=2, num_links=1)
+ port_mappings = [
+ eg.wim_port_mapping(0, 0),
+ eg.wim_port_mapping(0, 1)
+ ]
+ instance_action = eg.instance_action(action_id='ACTION-000')
+ for i, net in enumerate(instance_nets):
+ net['status'] = 'ACTIVE'
+ net['sdn_net_id'] = uuid('sdn-net%d' % i)
+
+ db_state += [{'instance_nets': instance_nets},
+ {'instance_wim_nets': eg.instance_wim_nets()},
+ {'wim_port_mappings': port_mappings},
+ {'instance_actions': instance_action}]
+
+ action = WanLinkCreate(
+ eg.wim_actions('CREATE', action_id='ACTION-000')[0])
+ # --> ensure it is in the database for updates --> #
+ action_record = action.as_record()
+ action_record['extra'] = json.dumps(action_record['extra'])
+ self.populate([{'vim_wim_actions': action_record}])
+
+ return db_state, action
+
+ @disable_foreign_keys
+ def test_process__sdn(self):
+ # Given we want 1 WAN link between 2 datacenters
+ # and the local network in each datacenter is already created
+ db_state, action = self.prepare_create__sdn()
+ self.populate(db_state)
+
+ instance_action = self.persist.get_by_uuid(
+ 'instance_actions', action.instance_action_id)
+ number_done = instance_action['number_done']
+ number_failed = instance_action['number_failed']
+
+ connector_patch = patch.object(
+ self.connector, 'create_connectivity_service',
+ lambda *_, **__: (uuid('random-id'), None))
+
+ ovim_patch = patch.object(
+ self.ovim, 'get_ports', MagicMock(return_value=[{
+ 'switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
+ 'switch_port': 1,
+ }]))
+
+ # If the connector works fine
+ with connector_patch, ovim_patch:
+ # When we try to process a CREATE action that refers to the same
+ # instance_scenario_id and sce_net_id
+ action.process(self.connector, self.persist, self.ovim)
+
+ # Then the action should be succeeded
+ db_action = self.persist.query_one('vim_wim_actions', WHERE={
+ 'instance_action_id': action.instance_action_id,
+ 'task_index': action.task_index})
+ self.assertEqual(db_action['status'], 'DONE')
+
+ instance_action = self.persist.get_by_uuid(
+ 'instance_actions', action.instance_action_id)
+ self.assertEqual(instance_action['number_done'], number_done + 1)
+ self.assertEqual(instance_action['number_failed'], number_failed)
+
+ @disable_foreign_keys
+ def test_process__sdn_fail(self):
+ # Given we want 1 WAN link between 2 datacenters
+ # and the local network in each datacenter is already created
+ db_state, action = self.prepare_create__sdn()
+ self.populate(db_state)
+
+ instance_action = self.persist.get_by_uuid(
+ 'instance_actions', action.instance_action_id)
+ number_done = instance_action['number_done']
+ number_failed = instance_action['number_failed']
+
+ connector_patch = patch.object(
+ self.connector, 'create_connectivity_service',
+ MagicMock(side_effect=WimConnectorError('foobar')))
+
+ ovim_patch = patch.object(
+ self.ovim, 'get_ports', MagicMock(return_value=[{
+ 'switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
+ 'switch_port': 1,
+ }]))
+
+ # If the connector throws an error
+ with connector_patch, ovim_patch:
+ # When we try to process a CREATE action that refers to the same
+ # instance_scenario_id and sce_net_id
+ action.process(self.connector, self.persist, self.ovim)
+
+ # Then the action should be fail
+ db_action = self.persist.query_one('vim_wim_actions', WHERE={
+ 'instance_action_id': action.instance_action_id,
+ 'task_index': action.task_index})
+ self.assertEqual(db_action['status'], 'FAILED')
+
+ instance_action = self.persist.get_by_uuid(
+ 'instance_actions', action.instance_action_id)
+ self.assertEqual(instance_action['number_done'], number_done)
+ self.assertEqual(instance_action['number_failed'], number_failed + 1)
+
+
+class TestDelete(TestActionsWithDb):
+ @disable_foreign_keys
+ def test_process__no_internal_id(self):
+ # Given no WAN link was created yet,
+ # when we try to process a DELETE action, with no wim_internal_id
+ action = WanLinkDelete(eg.wim_actions('DELETE')[0])
+ action.wim_internal_id = None
+ # -- ensure it is in the database for updates --> #
+ action_record = action.as_record()
+ action_record['extra'] = json.dumps(action_record['extra'])
+ self.populate([{'vim_wim_actions': action_record,
+ 'instance_wim_nets': eg.instance_wim_nets()}])
+ # <-- #
+ action.process(self.connector, self.persist, self.ovim)
+
+ # Then the action should succeed
+ assert action.is_done
+
+ def prepare_delete(self):
+ db_state = [{'nfvo_tenants': eg.tenant()}] + eg.wim_set()
+
+ instance_nets = eg.instance_nets(num_datacenters=2, num_links=1)
+ port_mappings = [
+ eg.wim_port_mapping(0, 0),
+ eg.wim_port_mapping(0, 1)
+ ]
+ instance_action = eg.instance_action(action_id='ACTION-000')
+ for i, net in enumerate(instance_nets):
+ net['status'] = 'ACTIVE'
+ net['sdn_net_id'] = uuid('sdn-net%d' % i)
+
+ db_state += [{'instance_nets': instance_nets},
+ {'instance_wim_nets': eg.instance_wim_nets()},
+ {'wim_port_mappings': port_mappings},
+ {'instance_actions': instance_action}]
+
+ action = WanLinkDelete(
+ eg.wim_actions('DELETE', action_id='ACTION-000')[0])
+ # --> ensure it is in the database for updates --> #
+ action_record = action.as_record()
+ action_record['extra'] = json.dumps(action_record['extra'])
+ self.populate([{'vim_wim_actions': action_record}])
+
+ return db_state, action
+
+ @disable_foreign_keys
+ def test_process(self):
+ # Given we want to delete 1 WAN link between 2 datacenters
+ db_state, action = self.prepare_delete()
+ self.populate(db_state)
+
+ instance_action = self.persist.get_by_uuid(
+ 'instance_actions', action.instance_action_id)
+ number_done = instance_action['number_done']
+ number_failed = instance_action['number_failed']
+
+ connector_patch = patch.object(
+ self.connector, 'delete_connectivity_service')
+
+ # If the connector works fine
+ with connector_patch:
+ # When we try to process a DELETE action that refers to the same
+ # instance_scenario_id and sce_net_id
+ action.process(self.connector, self.persist, self.ovim)
+
+ # Then the action should be succeeded
+ db_action = self.persist.query_one('vim_wim_actions', WHERE={
+ 'instance_action_id': action.instance_action_id,
+ 'task_index': action.task_index})
+ self.assertEqual(db_action['status'], 'DONE')
+
+ instance_action = self.persist.get_by_uuid(
+ 'instance_actions', action.instance_action_id)
+ self.assertEqual(instance_action['number_done'], number_done + 1)
+ self.assertEqual(instance_action['number_failed'], number_failed)
+
+ @disable_foreign_keys
+ def test_process__wan_link_error(self):
+ # Given we have a delete action that targets a wan link with an error
+ db_state, action = self.prepare_delete()
+ wan_link = [tables for tables in db_state
+ if tables.get('instance_wim_nets')][0]['instance_wim_nets']
+ from pprint import pprint
+ pprint(wan_link)
+ wan_link[0]['status'] = 'ERROR'
+ self.populate(db_state)
+
+ # When we try to process it
+ action.process(self.connector, self.persist, self.ovim)
+
+ # Then it should fail
+ assert action.is_failed
+
+ def create_action(self):
+ action = WanLinkCreate(
+ eg.wim_actions('CREATE', action_id='ACTION-000')[0])
+ # --> ensure it is in the database for updates --> #
+ action_record = action.as_record()
+ action_record['extra'] = json.dumps(action_record['extra'])
+ self.populate([{'vim_wim_actions': action_record}])
+
+ return action
+
+ @disable_foreign_keys
+ def test_create_and_delete(self):
+ # Given a CREATE action was well succeeded
+ db_state, delete_action = self.prepare_delete()
+ delete_action.save(self.persist, task_index=1)
+ self.populate(db_state)
+ create_action = self.create_action()
+
+ connector_patch = patch.multiple(
+ self.connector,
+ delete_connectivity_service=MagicMock(),
+ create_connectivity_service=(
+ lambda *_, **__: (uuid('random-id'), None)))
+
+ ovim_patch = patch.object(
+ self.ovim, 'get_ports', MagicMock(return_value=[{
+ 'switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
+ 'switch_port': 1,
+ }]))
+
+ with connector_patch, ovim_patch:
+ create_action.process(self.connector, self.persist, self.ovim)
+
+ # When we try to process a CREATE action that refers to the same
+ # instance_scenario_id and sce_net_id
+ with connector_patch:
+ delete_action.process(self.connector, self.persist, self.ovim)
+
+ # Then the DELETE action should be successful
+ db_action = self.persist.query_one('vim_wim_actions', WHERE={
+ 'instance_action_id': delete_action.instance_action_id,
+ 'task_index': delete_action.task_index})
+ self.assertEqual(db_action['status'], 'DONE')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/osm_ro/wim/tests/test_engine.py b/osm_ro/wim/tests/test_engine.py
new file mode 100644
index 0000000..6fb2d8c
--- /dev/null
+++ b/osm_ro/wim/tests/test_engine.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+from __future__ import unicode_literals
+
+import unittest
+
+from mock import MagicMock
+
+from . import fixtures as eg
+from ...tests.db_helpers import TestCaseWithDatabasePerTest, uuid
+from ..errors import NoWimConnectedToDatacenters
+from ..engine import WimEngine
+from ..persistence import WimPersistence
+
+
+class TestWimEngineDbMethods(TestCaseWithDatabasePerTest):
+ def setUp(self):
+ super(TestWimEngineDbMethods, self).setUp()
+ self.persist = WimPersistence(self.db)
+ self.engine = WimEngine(persistence=self.persist)
+ self.addCleanup(self.engine.stop_threads)
+
+ def populate(self, seeds=None):
+ super(TestWimEngineDbMethods, self).populate(
+ seeds or eg.consistent_set())
+
+ def test_find_common_wims(self):
+ # Given we have 2 WIM, 3 datacenters, but just 1 of the WIMs have
+ # access to them
+ self.populate([{'nfvo_tenants': [eg.tenant(0)]}] +
+ eg.wim_set(0, 0) +
+ eg.wim_set(1, 0) +
+ eg.datacenter_set(0, 0) +
+ eg.datacenter_set(1, 0) +
+ eg.datacenter_set(2, 0) +
+ [{'wim_port_mappings': [
+ eg.wim_port_mapping(0, 0),
+ eg.wim_port_mapping(0, 1),
+ eg.wim_port_mapping(0, 2)]}])
+
+ # When we retrieve the wims interconnecting some datacenters
+ wim_ids = self.engine.find_common_wims(
+ [uuid('dc0'), uuid('dc1'), uuid('dc2')], tenant='tenant0')
+
+ # Then we should have just the first wim
+ self.assertEqual(len(wim_ids), 1)
+ self.assertEqual(wim_ids[0], uuid('wim0'))
+
+ def test_find_common_wims_multiple(self):
+ # Given we have 2 WIM, 3 datacenters, and all the WIMs have access to
+ # all datacenters
+ self.populate([{'nfvo_tenants': [eg.tenant(0)]}] +
+ eg.wim_set(0, 0) +
+ eg.wim_set(1, 0) +
+ eg.datacenter_set(0, 0) +
+ eg.datacenter_set(1, 0) +
+ eg.datacenter_set(2, 0) +
+ [{'wim_port_mappings': [
+ eg.wim_port_mapping(0, 0),
+ eg.wim_port_mapping(0, 1),
+ eg.wim_port_mapping(0, 2),
+ eg.wim_port_mapping(1, 0),
+ eg.wim_port_mapping(1, 1),
+ eg.wim_port_mapping(1, 2)]}])
+
+ # When we retrieve the wims interconnecting tree datacenters
+ wim_ids = self.engine.find_common_wims(
+ [uuid('dc0'), uuid('dc1'), uuid('dc2')], tenant='tenant0')
+
+ # Then we should have all the wims
+ self.assertEqual(len(wim_ids), 2)
+ self.assertItemsEqual(wim_ids, [uuid('wim0'), uuid('wim1')])
+
+ def test_find_common_wim(self):
+ # Given we have 1 WIM, 3 datacenters but the WIM have access to just 2
+ # of them
+ self.populate([{'nfvo_tenants': [eg.tenant(0)]}] +
+ eg.wim_set(0, 0) +
+ eg.datacenter_set(0, 0) +
+ eg.datacenter_set(1, 0) +
+ eg.datacenter_set(2, 0) +
+ [{'wim_port_mappings': [
+ eg.wim_port_mapping(0, 0),
+ eg.wim_port_mapping(0, 1)]}])
+
+ # When we retrieve the common wim for the 2 datacenter that are
+ # interconnected
+ wim_id = self.engine.find_common_wim(
+ [uuid('dc0'), uuid('dc1')], tenant='tenant0')
+
+ # Then we should find the wim
+ self.assertEqual(wim_id, uuid('wim0'))
+
+ # When we try to retrieve the common wim for the all the datacenters
+ # Then a NoWimConnectedToDatacenters exception should be raised
+ with self.assertRaises(NoWimConnectedToDatacenters):
+ self.engine.find_common_wim(
+ [uuid('dc0'), uuid('dc1'), uuid('dc2')], tenant='tenant0')
+
+ def test_find_common_wim__different_tenants(self):
+ # Given we have 1 WIM and 2 datacenters connected but the WIMs don't
+ # belong to the tenant we have access to...
+ self.populate([{'nfvo_tenants': [eg.tenant(0), eg.tenant(1)]}] +
+ eg.wim_set(0, 0) +
+ eg.datacenter_set(0, 0) +
+ eg.datacenter_set(1, 0) +
+ [{'wim_port_mappings': [
+ eg.wim_port_mapping(0, 0),
+ eg.wim_port_mapping(0, 1)]}])
+
+ # When we retrieve the common wim for the 2 datacenter that are
+ # interconnected, but using another tenant,
+ # Then we should get an exception
+ with self.assertRaises(NoWimConnectedToDatacenters):
+ self.engine.find_common_wim(
+ [uuid('dc0'), uuid('dc1')], tenant='tenant1')
+
+
+class TestWimEngine(unittest.TestCase):
+ def test_derive_wan_link(self):
+ # Given we have 2 datacenters connected by the same WIM, with port
+ # mappings registered
+ mappings = [eg.processed_port_mapping(0, 0),
+ eg.processed_port_mapping(0, 1)]
+ persist = MagicMock(
+ get_wim_port_mappings=MagicMock(return_value=mappings))
+
+ engine = WimEngine(persistence=persist)
+ self.addCleanup(engine.stop_threads)
+
+ # When we receive a list of 4 instance nets, representing
+ # 2 VLDs connecting 2 datacenters each
+ instance_nets = eg.instance_nets(2, 2)
+ wan_links = engine.derive_wan_links(
+ instance_nets, uuid('tenant0'))
+
+ # Then we should derive 2 wan_links with the same instance_scenario_id
+ # and different scenario_network_id
+ self.assertEqual(len(wan_links), 2)
+ for link in wan_links:
+ self.assertEqual(link['instance_scenario_id'], uuid('nsr0'))
+ # Each VLD needs a network to be created in each datacenter
+ self.assertItemsEqual([l['sce_net_id'] for l in wan_links],
+ [uuid('vld0'), uuid('vld1')])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/osm_ro/wim/tests/test_http_handler.py b/osm_ro/wim/tests/test_http_handler.py
new file mode 100644
index 0000000..04577e4
--- /dev/null
+++ b/osm_ro/wim/tests/test_http_handler.py
@@ -0,0 +1,508 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+from __future__ import unicode_literals
+
+import unittest
+
+import bottle
+from mock import MagicMock, patch
+from webtest import TestApp
+
+from . import fixtures as eg # "examples"
+from ...http_tools.errors import Conflict, Not_Found
+from ...tests.db_helpers import TestCaseWithDatabasePerTest, uuid
+from ...utils import merge_dicts
+from ..http_handler import WimHandler
+
+OK = 200
+
+
+@patch('osm_ro.wim.wim_thread.CONNECTORS', MagicMock()) # Avoid external calls
+@patch('osm_ro.wim.wim_thread.WimThread.start', MagicMock()) # Avoid running
+class TestHttpHandler(TestCaseWithDatabasePerTest):
+ def setUp(self):
+ super(TestHttpHandler, self).setUp()
+ bottle.debug(True)
+ handler = WimHandler(db=self.db)
+ self.engine = handler.engine
+ self.addCleanup(self.engine.stop_threads)
+ self.app = TestApp(handler.wsgi_app)
+
+ def populate(self, seeds=None):
+ super(TestHttpHandler, self).populate(seeds or eg.consistent_set())
+
+ def test_list_wims(self):
+ # Given some wims are registered in the database
+ self.populate()
+ # when a GET /<tenant_id>/wims request arrives
+ tenant_id = uuid('tenant0')
+ response = self.app.get('/{}/wims'.format(tenant_id))
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ # and all the registered wims should be present
+ retrieved_wims = {v['name']: v for v in response.json['wims']}
+ for name in retrieved_wims:
+ identifier = int(name.replace('wim', ''))
+ self.assertDictContainsSubset(
+ eg.wim(identifier), retrieved_wims[name])
+
+ def test_show_wim(self):
+ # Given some wims are registered in the database
+ self.populate()
+ # when a GET /<tenant_id>/wims/<wim_id> request arrives
+ tenant_id = uuid('tenant0')
+ wim_id = uuid('wim1')
+ response = self.app.get('/{}/wims/{}'.format(tenant_id, wim_id))
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ # and the registered wim (wim1) should be present
+ self.assertDictContainsSubset(eg.wim(1), response.json['wim'])
+ # Moreover, it also works with tenant_id = all
+ response = self.app.get('/any/wims/{}'.format(wim_id))
+ self.assertEqual(response.status_code, OK)
+ self.assertDictContainsSubset(eg.wim(1), response.json['wim'])
+
+ def test_show_wim__wim_doesnt_exists(self):
+ # Given wim_id does not refer to any already registered wim
+ self.populate()
+ # when a GET /<tenant_id>/wims/<wim_id> request arrives
+ tenant_id = uuid('tenant0')
+ wim_id = uuid('wim999')
+ response = self.app.get(
+ '/{}/wims/{}'.format(tenant_id, wim_id),
+ expect_errors=True)
+
+ # then the result should not be well succeeded
+ self.assertEqual(response.status_code, Not_Found)
+
+ def test_show_wim__tenant_doesnt_exists(self):
+ # Given wim_id does not refer to any already registered wim
+ self.populate()
+ # when a GET /<tenant_id>/wims/<wim_id> request arrives
+ tenant_id = uuid('tenant999')
+ wim_id = uuid('wim0')
+ response = self.app.get(
+ '/{}/wims/{}'.format(tenant_id, wim_id),
+ expect_errors=True)
+
+ # then the result should not be well succeeded
+ self.assertEqual(response.status_code, Not_Found)
+
+ def test_edit_wim(self):
+ # Given a WIM exists in the database
+ self.populate()
+ # when a PUT /wims/<wim_id> request arrives
+ wim_id = uuid('wim1')
+ response = self.app.put_json('/wims/{}'.format(wim_id), {
+ 'wim': {'name': 'My-New-Name'}})
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ # and the registered wim (wim1) should be present
+ self.assertDictContainsSubset(
+ merge_dicts(eg.wim(1), name='My-New-Name'),
+ response.json['wim'])
+
+ def test_delete_wim(self):
+ # Given a WIM exists in the database
+ self.populate()
+ num_accounts = self.count('wim_accounts')
+ num_associations = self.count('wim_nfvo_tenants')
+ num_mappings = self.count('wim_port_mappings')
+
+ with self.engine.threads_running():
+ num_threads = len(self.engine.threads)
+ # when a DELETE /wims/<wim_id> request arrives
+ wim_id = uuid('wim1')
+ response = self.app.delete('/wims/{}'.format(wim_id))
+ num_threads_after = len(self.engine.threads)
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ self.assertIn('deleted', response.json['result'])
+ # and the registered wim1 should be deleted
+ response = self.app.get(
+ '/any/wims/{}'.format(wim_id),
+ expect_errors=True)
+ self.assertEqual(response.status_code, Not_Found)
+ # and all the dependent records in other tables should be deleted:
+ # wim_accounts, wim_nfvo_tenants, wim_port_mappings
+ self.assertEqual(self.count('wim_nfvo_tenants'),
+ num_associations - eg.NUM_TENANTS)
+ self.assertLess(self.count('wim_port_mappings'), num_mappings)
+ self.assertEqual(self.count('wim_accounts'),
+ num_accounts - eg.NUM_TENANTS)
+ # And the threads associated with the wim accounts should be stopped
+ self.assertEqual(num_threads_after, num_threads - eg.NUM_TENANTS)
+
+ def test_create_wim(self):
+ # Given no WIM exists yet
+ # when a POST /wims request arrives with the right payload
+ response = self.app.post_json('/wims', {'wim': eg.wim(999)})
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ self.assertEqual(response.json['wim']['name'], 'wim999')
+
+ def test_create_wim_account(self):
+ # Given a WIM and a NFVO tenant exist but are not associated
+ self.populate([{'wims': [eg.wim(0)]},
+ {'nfvo_tenants': [eg.tenant(0)]}])
+
+ with self.engine.threads_running():
+ num_threads = len(self.engine.threads)
+ # when a POST /<tenant_id>/wims/<wim_id> arrives
+ response = self.app.post_json(
+ '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')),
+ {'wim_account': eg.wim_account(0, 0)})
+
+ num_threads_after = len(self.engine.threads)
+
+ # then a new thread should be created
+ self.assertEqual(num_threads_after, num_threads + 1)
+
+ # and the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ self.assertEqual(response.json['wim_account']['name'], 'wim-account00')
+
+ # and a new association record should be created
+ association = self.db.get_rows(FROM='wim_nfvo_tenants')
+ assert association
+ self.assertEqual(len(association), 1)
+ self.assertEqual(association[0]['wim_id'], uuid('wim0'))
+ self.assertEqual(association[0]['nfvo_tenant_id'], uuid('tenant0'))
+ self.assertEqual(association[0]['wim_account_id'],
+ response.json['wim_account']['uuid'])
+
+ def test_create_wim_account__existing_account(self):
+ # Given a WIM, a WIM account and a NFVO tenants exist
+ # But the NFVO and the WIM are not associated
+ self.populate([
+ {'wims': [eg.wim(0)]},
+ {'nfvo_tenants': [eg.tenant(0)]},
+ {'wim_accounts': [eg.wim_account(0, 0)]}])
+
+ # when a POST /<tenant_id>/wims/<wim_id> arrives
+ # and it refers to an existing wim account
+ response = self.app.post_json(
+ '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')),
+ {'wim_account': {'name': 'wim-account00'}})
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ # and the association should be created
+ association = self.db.get_rows(
+ FROM='wim_nfvo_tenants',
+ WHERE={'wim_id': uuid('wim0'),
+ 'nfvo_tenant_id': uuid('tenant0')})
+ assert association
+ self.assertEqual(len(association), 1)
+ # but no new wim_account should be created
+ wim_accounts = self.db.get_rows(FROM='wim_accounts')
+ self.assertEqual(len(wim_accounts), 1)
+ self.assertEqual(wim_accounts[0]['name'], 'wim-account00')
+
+ def test_create_wim_account__existing_account__differing(self):
+ # Given a WIM, a WIM account and a NFVO tenants exist
+ # But the NFVO and the WIM are not associated
+ self.populate([
+ {'wims': [eg.wim(0)]},
+ {'nfvo_tenants': [eg.tenant(0)]},
+ {'wim_accounts': [eg.wim_account(0, 0)]}])
+
+ # when a POST /<tenant_id>/wims/<wim_id> arrives
+ # and it refers to an existing wim account,
+ # but with different fields
+ response = self.app.post_json(
+ '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
+ 'wim_account': {
+ 'name': 'wim-account00',
+ 'user': 'john',
+ 'password': 'abc123'}},
+ expect_errors=True)
+
+ # then the request should not be well succeeded
+ self.assertEqual(response.status_code, Conflict)
+ # some useful message should be displayed
+ response.mustcontain('attempt to overwrite', 'user', 'password')
+ # and the association should not be created
+ association = self.db.get_rows(
+ FROM='wim_nfvo_tenants',
+ WHERE={'wim_id': uuid('wim0'),
+ 'nfvo_tenant_id': uuid('tenant0')})
+ assert not association
+
+ def test_create_wim_account__association_already_exists(self):
+ # Given a WIM, a WIM account and a NFVO tenants exist
+ # and are correctly associated
+ self.populate()
+ num_assoc_before = self.count('wim_nfvo_tenants')
+
+ # when a POST /<tenant_id>/wims/<wim_id> arrives trying to connect a
+ # WIM and a tenant for the second time
+ response = self.app.post_json(
+ '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
+ 'wim_account': {
+ 'user': 'user999',
+ 'password': 'password999'}},
+ expect_errors=True)
+
+ # then the request should not be well succeeded
+ self.assertEqual(response.status_code, Conflict)
+ # the message should be useful
+ response.mustcontain('There is already', uuid('wim0'), uuid('tenant0'))
+
+ num_assoc_after = self.count('wim_nfvo_tenants')
+
+ # and the number of association record should not be increased
+ self.assertEqual(num_assoc_before, num_assoc_after)
+
+ def test_create_wim__tenant_doesnt_exist(self):
+ # Given a tenant not exists
+ self.populate()
+
+ # But the user tries to create a wim_account anyway
+ response = self.app.post_json(
+ '/{}/wims/{}'.format(uuid('tenant999'), uuid('wim0')), {
+ 'wim_account': {
+ 'user': 'user999',
+ 'password': 'password999'}},
+ expect_errors=True)
+
+ # then the request should not be well succeeded
+ self.assertEqual(response.status_code, Not_Found)
+ # the message should be useful
+ response.mustcontain('No record was found', uuid('tenant999'))
+
+ def test_create_wim__wim_doesnt_exist(self):
+ # Given a tenant not exists
+ self.populate()
+
+ # But the user tries to create a wim_account anyway
+ response = self.app.post_json(
+ '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim999')), {
+ 'wim_account': {
+ 'user': 'user999',
+ 'password': 'password999'}},
+ expect_errors=True)
+
+ # then the request should not be well succeeded
+ self.assertEqual(response.status_code, Not_Found)
+ # the message should be useful
+ response.mustcontain('No record was found', uuid('wim999'))
+
+ def test_update_wim_account(self):
+ # Given a WIM account connecting a tenant and a WIM exists
+ self.populate()
+
+ with self.engine.threads_running():
+ num_threads = len(self.engine.threads)
+
+ thread = self.engine.threads[uuid('wim-account00')]
+ reload = MagicMock(wraps=thread.reload)
+
+ with patch.object(thread, 'reload', reload):
+ # when a PUT /<tenant_id>/wims/<wim_id> arrives
+ response = self.app.put_json(
+ '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
+ 'wim_account': {
+ 'name': 'account888',
+ 'user': 'user888'}})
+
+ num_threads_after = len(self.engine.threads)
+
+ # then the wim thread should be restarted
+ reload.assert_called_once()
+ # and no thread should be added or removed
+ self.assertEqual(num_threads_after, num_threads)
+
+ # and the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ self.assertEqual(response.json['wim_account']['name'], 'account888')
+ self.assertEqual(response.json['wim_account']['user'], 'user888')
+
+ def test_update_wim_account__multiple(self):
+ # Given a WIM account connected to several tenants
+ self.populate()
+
+ with self.engine.threads_running():
+ # when a PUT /any/wims/<wim_id> arrives
+ response = self.app.put_json(
+ '/any/wims/{}'.format(uuid('wim0')), {
+ 'wim_account': {
+ 'user': 'user888',
+ 'config': {'x': 888}}})
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ self.assertEqual(len(response.json['wim_accounts']), eg.NUM_TENANTS)
+
+ for account in response.json['wim_accounts']:
+ self.assertEqual(account['user'], 'user888')
+ self.assertEqual(account['config']['x'], 888)
+
+ def test_delete_wim_account(self):
+ # Given a WIM account exists and it is connected to a tenant
+ self.populate()
+
+ num_accounts_before = self.count('wim_accounts')
+
+ with self.engine.threads_running():
+ thread = self.engine.threads[uuid('wim-account00')]
+ exit = MagicMock(wraps=thread.exit)
+ num_threads = len(self.engine.threads)
+
+ with patch.object(thread, 'exit', exit):
+ # when a PUT /<tenant_id>/wims/<wim_id> arrives
+ response = self.app.delete_json(
+ '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')))
+
+ num_threads_after = len(self.engine.threads)
+
+ # then the wim thread should exit
+ self.assertEqual(num_threads_after, num_threads - 1)
+ exit.assert_called_once()
+
+ # and the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ response.mustcontain('account `wim-account00` deleted')
+
+ # and the number of wim_accounts should decrease
+ num_accounts_after = self.count('wim_accounts')
+ self.assertEqual(num_accounts_after, num_accounts_before - 1)
+
+ def test_delete_wim_account__multiple(self):
+ # Given a WIM account exists and it is connected to several tenants
+ self.populate()
+
+ num_accounts_before = self.count('wim_accounts')
+
+ with self.engine.threads_running():
+ # when a PUT /<tenant_id>/wims/<wim_id> arrives
+ response = self.app.delete_json(
+ '/any/wims/{}'.format(uuid('wim0')))
+
+ # then the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ response.mustcontain('account `wim-account00` deleted')
+ response.mustcontain('account `wim-account10` deleted')
+
+ # and the number of wim_accounts should decrease
+ num_accounts_after = self.count('wim_accounts')
+ self.assertEqual(num_accounts_after,
+ num_accounts_before - eg.NUM_TENANTS)
+
+ def test_delete_wim_account__doesnt_exist(self):
+ # Given we have a tenant that is not connected to a WIM
+ self.populate()
+ tenant = {'uuid': uuid('tenant888'), 'name': 'tenant888'}
+ self.populate([{'nfvo_tenants': [tenant]}])
+
+ num_accounts_before = self.count('wim_accounts')
+
+ # when a PUT /<tenant_id>/wims/<wim_id> arrives
+ response = self.app.delete(
+ '/{}/wims/{}'.format(uuid('tenant888'), uuid('wim0')),
+ expect_errors=True)
+
+ # then the request should not succeed
+ self.assertEqual(response.status_code, Not_Found)
+
+ # and the number of wim_accounts should not decrease
+ num_accounts_after = self.count('wim_accounts')
+ self.assertEqual(num_accounts_after, num_accounts_before)
+
+ def test_create_port_mappings(self):
+ # Given we have a wim and datacenter without any port mappings
+ self.populate([{'nfvo_tenants': eg.tenant(0)}] +
+ eg.datacenter_set(888, 0) +
+ eg.wim_set(999, 0))
+
+ # when a POST /<tenant_id>/wims/<wim_id>/port_mapping arrives
+ response = self.app.post_json(
+ '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim999')),
+ {'wim_port_mapping': [{
+ 'datacenter_name': 'dc888',
+ 'pop_wan_mappings': [
+ {'pop_switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
+ 'pop_switch_port': 1,
+ 'wan_service_mapping_info': {
+ 'mapping_type': 'dpid-port',
+ 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:BB',
+ 'wan_switch_port': 1
+ }}
+ ]}
+ ]})
+
+ # the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ # and port mappings should be stored in the database
+ port_mapping = self.db.get_rows(FROM='wim_port_mappings')
+ self.assertEqual(len(port_mapping), 1)
+
+ def test_get_port_mappings(self):
+ # Given WIMS and datacenters exist with port mappings between them
+ self.populate()
+ # when a GET /<tenant_id>/wims/<wim_id>/port_mapping arrives
+ response = self.app.get(
+ '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim0')))
+ # the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ # and we should see port mappings for each WIM, datacenter pair
+ mappings = response.json['wim_port_mapping']
+ self.assertEqual(len(mappings), eg.NUM_DATACENTERS)
+ # ^ In the fixture set all the datacenters are connected to all wims
+
+ def test_delete_port_mappings(self):
+ # Given WIMS and datacenters exist with port mappings between them
+ self.populate()
+ num_mappings_before = self.count('wim_port_mappings')
+
+ # when a DELETE /<tenant_id>/wims/<wim_id>/port_mapping arrives
+ response = self.app.delete(
+ '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim0')))
+ # the request should be well succeeded
+ self.assertEqual(response.status_code, OK)
+ # and the number of port mappings should decrease
+ num_mappings_after = self.count('wim_port_mappings')
+ self.assertEqual(num_mappings_after,
+ num_mappings_before - eg.NUM_DATACENTERS)
+ # ^ In the fixture set all the datacenters are connected to all wims
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/osm_ro/wim/tests/test_persistence.py b/osm_ro/wim/tests/test_persistence.py
new file mode 100644
index 0000000..d09a116
--- /dev/null
+++ b/osm_ro/wim/tests/test_persistence.py
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+from __future__ import unicode_literals
+
+import unittest
+from itertools import chain
+from types import StringType
+
+from six.moves import range
+
+from . import fixtures as eg
+from ...tests.db_helpers import (
+ TestCaseWithDatabasePerTest,
+ disable_foreign_keys,
+ uuid
+)
+from ..persistence import (
+ WimPersistence,
+ hide_confidential_fields,
+ serialize_fields,
+ unserialize_fields
+)
+
+
+class TestPersistenceUtils(unittest.TestCase):
+ def test_hide_confidential_fields(self):
+ example = {
+ 'password': '123456',
+ 'nested.password': '123456',
+ 'nested.secret': None,
+ }
+ result = hide_confidential_fields(example,
+ fields=('password', 'secret'))
+ for field in 'password', 'nested.password':
+ assert result[field].startswith('***')
+ self.assertIs(result['nested.secret'], None)
+
+ def test_serialize_fields(self):
+ example = {
+ 'config': dict(x=1),
+ 'nested.info': [1, 2, 3],
+ 'nested.config': None
+ }
+ result = serialize_fields(example, fields=('config', 'info'))
+ for field in 'config', 'nested.info':
+ self.assertIsInstance(result[field], StringType)
+ self.assertIs(result['nested.config'], None)
+
+ def test_unserialize_fields(self):
+ example = {
+ 'config': '{"x": 1}',
+ 'nested.info': '[1,2,3]',
+ 'nested.config': None,
+ 'confidential.info': '{"password": "abcdef"}'
+ }
+ result = unserialize_fields(example, fields=('config', 'info'))
+ self.assertEqual(result['config'], dict(x=1))
+ self.assertEqual(result['nested.info'], [1, 2, 3])
+ self.assertIs(result['nested.config'], None)
+ self.assertNotEqual(result['confidential.info']['password'], 'abcdef')
+ assert result['confidential.info']['password'].startswith('***')
+
+
+class TestWimPersistence(TestCaseWithDatabasePerTest):
+ def setUp(self):
+ super(TestWimPersistence, self).setUp()
+ self.persist = WimPersistence(self.db)
+
+ def populate(self, seeds=None):
+ super(TestWimPersistence, self).populate(seeds or eg.consistent_set())
+
+ def test_query_offset(self):
+ # Given a database contains 4 records
+ self.populate([{'wims': [eg.wim(i) for i in range(4)]}])
+
+ # When we query using a limit of 2 and a offset of 1
+ results = self.persist.query('wims',
+ ORDER_BY='name', LIMIT=2, OFFSET=1)
+ # Then we should have 2 results, skipping the first record
+ names = [r['name'] for r in results]
+ self.assertItemsEqual(names, ['wim1', 'wim2'])
+
+ def test_get_wim_account_by_wim_tenant(self):
+ # Given a database contains WIM accounts associated to Tenants
+ self.populate()
+
+ # when we retrieve the account using wim and tenant
+ wim_account = self.persist.get_wim_account_by(
+ uuid('wim0'), uuid('tenant0'))
+
+ # then the right record should be returned
+ self.assertEqual(wim_account['uuid'], uuid('wim-account00'))
+ self.assertEqual(wim_account['name'], 'wim-account00')
+ self.assertEqual(wim_account['user'], 'user00')
+
+ def test_get_wim_account_by_wim_tenant__names(self):
+ # Given a database contains WIM accounts associated to Tenants
+ self.populate()
+
+ # when we retrieve the account using wim and tenant
+ wim_account = self.persist.get_wim_account_by(
+ 'wim0', 'tenant0')
+
+ # then the right record should be returned
+ self.assertEqual(wim_account['uuid'], uuid('wim-account00'))
+ self.assertEqual(wim_account['name'], 'wim-account00')
+ self.assertEqual(wim_account['user'], 'user00')
+
+ def test_get_wim_accounts_by_wim(self):
+ # Given a database contains WIM accounts associated to Tenants
+ self.populate()
+
+ # when we retrieve the accounts using wim
+ wim_accounts = self.persist.get_wim_accounts_by(uuid('wim0'))
+
+ # then the right records should be returned
+ self.assertEqual(len(wim_accounts), eg.NUM_TENANTS)
+ for account in wim_accounts:
+ self.assertEqual(account['wim_id'], uuid('wim0'))
+
+ def test_get_wim_port_mappings(self):
+ # Given a database with WIMs, datacenters and port-mappings
+ self.populate()
+
+ # when we retrieve the port mappings for a list of datacenters
+ # using either names or uuids
+ for criteria in ([uuid('dc0'), uuid('dc1')], ['dc0', 'dc1']):
+ mappings = self.persist.get_wim_port_mappings(datacenter=criteria)
+
+ # then each result should have a datacenter_id
+ datacenters = [m['datacenter_id'] for m in mappings]
+ for datacenter in datacenters:
+ self.assertIn(datacenter, [uuid('dc0'), uuid('dc1')])
+
+ # a wim_id
+ wims = [m['wim_id'] for m in mappings]
+ for wim in wims:
+ self.assertIsNot(wim, None)
+
+ # and a array of pairs 'wan' <> 'pop' connections
+ pairs = chain(*(m['wan_pop_port_mappings'] for m in mappings))
+ self.assertEqual(len(list(pairs)), 2 * eg.NUM_WIMS)
+
+ def test_get_wim_port_mappings_multiple(self):
+ # Given we have more then one connection in a datacenter managed by the
+ # WIM
+ self.populate()
+ self.populate([{
+ 'wim_port_mappings': [
+ eg.wim_port_mapping(
+ 0, 0,
+ pop_dpid='CC:CC:CC:CC:CC:CC:CC:CC',
+ wan_dpid='DD:DD:DD:DD:DD:DD:DD:DD'),
+ eg.wim_port_mapping(
+ 0, 0,
+ pop_dpid='EE:EE:EE:EE:EE:EE:EE:EE',
+ wan_dpid='FF:FF:FF:FF:FF:FF:FF:FF')]}])
+
+ # when we retrieve the port mappings for the wim and datacenter:
+ mappings = (
+ self.persist.get_wim_port_mappings(wim='wim0', datacenter='dc0'))
+
+ # then it should return just a single result, grouped by wim and
+ # datacenter
+ self.assertEqual(len(mappings), 1)
+ self.assertEqual(mappings[0]['wim_id'], uuid('wim0'))
+ self.assertEqual(mappings[0]['datacenter_id'], uuid('dc0'))
+
+ self.assertEqual(len(mappings[0]['wan_pop_port_mappings']), 3)
+
+ # when we retreive the mappings for more then one wim/datacenter
+ # the grouping should still work properly
+ mappings = self.persist.get_wim_port_mappings(
+ wim=['wim0', 'wim1'], datacenter=['dc0', 'dc1'])
+ self.assertEqual(len(mappings), 4)
+ pairs = chain(*(m['wan_pop_port_mappings'] for m in mappings))
+ self.assertEqual(len(list(pairs)), 6)
+
+ def test_get_actions_in_group(self):
+ # Given a good number of wim actions exist in the database
+ kwargs = {'action_id': uuid('action0')}
+ actions = (eg.wim_actions('CREATE', num_links=8, **kwargs) +
+ eg.wim_actions('FIND', num_links=8, **kwargs) +
+ eg.wim_actions('START', num_links=8, **kwargs))
+ for i, action in enumerate(actions):
+ action['task_index'] = i
+
+ self.populate([
+ {'nfvo_tenants': eg.tenant()}
+ ] + eg.wim_set() + [
+ {'instance_actions': eg.instance_action(**kwargs)},
+ {'vim_wim_actions': actions}
+ ])
+
+ # When we retrieve them in groups
+ limit = 5
+ results = self.persist.get_actions_in_groups(
+ uuid('wim-account00'), ['instance_wim_nets'], group_limit=limit)
+
+ # Then we should have N groups where N == limit
+ self.assertEqual(len(results), limit)
+ for _, task_list in results:
+ # And since for each link we have create 3 actions (create, find,
+ # start), we should find them in each group
+ self.assertEqual(len(task_list), 3)
+
+ @disable_foreign_keys
+ def test_update_instance_action_counters(self):
+ # Given we have one instance action in the database with 2 incomplete
+ # tasks
+ action = eg.instance_action(num_tasks=2)
+ self.populate([{'instance_actions': action}])
+ # When we update the done counter by 0, nothing should happen
+ self.persist.update_instance_action_counters(action['uuid'], done=0)
+ result = self.persist.get_by_uuid('instance_actions', action['uuid'])
+ self.assertEqual(result['number_done'], 0)
+ self.assertEqual(result['number_failed'], 0)
+ # When we update the done counter by 2, number_done should be 2
+ self.persist.update_instance_action_counters(action['uuid'], done=2)
+ result = self.persist.get_by_uuid('instance_actions', action['uuid'])
+ self.assertEqual(result['number_done'], 2)
+ self.assertEqual(result['number_failed'], 0)
+ # When we update the done counter by -1, and the failed counter by 1
+ self.persist.update_instance_action_counters(
+ action['uuid'], done=-1, failed=1)
+ # Then we should see 1 and 1
+ result = self.persist.get_by_uuid('instance_actions', action['uuid'])
+ self.assertEqual(result['number_done'], 1)
+ self.assertEqual(result['number_failed'], 1)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/osm_ro/wim/tests/test_wim_thread.py b/osm_ro/wim/tests/test_wim_thread.py
new file mode 100644
index 0000000..6d61848
--- /dev/null
+++ b/osm_ro/wim/tests/test_wim_thread.py
@@ -0,0 +1,332 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+from __future__ import unicode_literals, print_function
+
+import unittest
+from difflib import unified_diff
+from operator import itemgetter
+from time import time
+
+import json
+
+from mock import MagicMock, patch
+
+from . import fixtures as eg
+from ...tests.db_helpers import (
+ TestCaseWithDatabasePerTest,
+ disable_foreign_keys,
+ uuid
+)
+from ..engine import WimEngine
+from ..persistence import WimPersistence
+from ..wim_thread import WimThread
+
+
+ignore_connector = patch('osm_ro.wim.wim_thread.CONNECTORS', MagicMock())
+
+
+def _repr(value):
+ return json.dumps(value, indent=4, sort_keys=True)
+
+
+@ignore_connector
+class TestWimThreadWithDb(TestCaseWithDatabasePerTest):
+ def setUp(self):
+ super(TestWimThreadWithDb, self).setUp()
+ self.persist = WimPersistence(self.db)
+ wim = eg.wim(0)
+ account = eg.wim_account(0, 0)
+ account['wim'] = wim
+ self.thread = WimThread(self.persist, account)
+ self.thread.connector = MagicMock()
+
+ def assertTasksEqual(self, left, right):
+ fields = itemgetter('item', 'item_id', 'action', 'status')
+ left_ = (t.as_dict() for t in left)
+ left_ = [fields(t) for t in left_]
+ right_ = [fields(t) for t in right]
+
+ try:
+ self.assertItemsEqual(left_, right_)
+ except AssertionError:
+ print('left', _repr(left))
+ print('left', len(left_), 'items')
+ print('right', len(right_), 'items')
+ result = list(unified_diff(_repr(sorted(left_)).split('\n'),
+ _repr(sorted(right_)).split('\n'),
+ 'left', 'right'))
+ print('diff:\n', '\n'.join(result))
+ raise
+
+ def test_reload_actions__all_create(self):
+ # Given we have 3 CREATE actions stored in the database
+ actions = eg.wim_actions('CREATE',
+ action_id=uuid('action0'), num_links=3)
+ self.populate([
+ {'nfvo_tenants': eg.tenant()}
+ ] + eg.wim_set() + [
+ {'instance_actions':
+ eg.instance_action(action_id=uuid('action0'))},
+ {'vim_wim_actions': actions}
+ ])
+
+ # When we reload the tasks
+ self.thread.reload_actions()
+ # All of them should be inserted as pending
+ self.assertTasksEqual(self.thread.pending_tasks, actions)
+
+ def test_reload_actions__all_refresh(self):
+ # Given just DONE tasks are in the database
+ actions = eg.wim_actions(status='DONE',
+ action_id=uuid('action0'), num_links=3)
+ self.populate([
+ {'nfvo_tenants': eg.tenant()}
+ ] + eg.wim_set() + [
+ {'instance_actions':
+ eg.instance_action(action_id=uuid('action0'))},
+ {'vim_wim_actions': actions}
+ ])
+
+ # When we reload the tasks
+ self.thread.reload_actions()
+ # All of them should be inserted as refresh
+ self.assertTasksEqual(self.thread.refresh_tasks, actions)
+
+ def test_reload_actions__grouped(self):
+ # Given we have 2 tasks for the same item in the database
+ kwargs = {'action_id': uuid('action0')}
+ actions = (eg.wim_actions('CREATE', **kwargs) +
+ eg.wim_actions('FIND', **kwargs))
+ for i, action in enumerate(actions):
+ action['task_index'] = i
+
+ self.populate([
+ {'nfvo_tenants': eg.tenant()}
+ ] + eg.wim_set() + [
+ {'instance_actions': eg.instance_action(**kwargs)},
+ {'vim_wim_actions': actions}
+ ])
+
+ # When we reload the tasks
+ self.thread.reload_actions()
+ # Just one group should be created
+ self.assertEqual(len(self.thread.grouped_tasks.values()), 1)
+
+ def test_reload_actions__delete_scheduled(self):
+ # Given we have 3 tasks for the same item in the database, but one of
+ # them is a DELETE task and it is SCHEDULED
+ kwargs = {'action_id': uuid('action0')}
+ actions = (eg.wim_actions('CREATE', **kwargs) +
+ eg.wim_actions('FIND', **kwargs) +
+ eg.wim_actions('DELETE', status='SCHEDULED', **kwargs))
+ for i, action in enumerate(actions):
+ action['task_index'] = i
+
+ self.populate([
+ {'nfvo_tenants': eg.tenant()}
+ ] + eg.wim_set() + [
+ {'instance_actions': eg.instance_action(**kwargs)},
+ {'vim_wim_actions': actions}
+ ])
+
+ # When we reload the tasks
+ self.thread.reload_actions()
+ # Just one group should be created
+ self.assertEqual(len(self.thread.grouped_tasks.values()), 1)
+
+ def test_reload_actions__delete_done(self):
+ # Given we have 3 tasks for the same item in the database, but one of
+ # them is a DELETE task and it is not SCHEDULED
+ kwargs = {'action_id': uuid('action0')}
+ actions = (eg.wim_actions('CREATE', **kwargs) +
+ eg.wim_actions('FIND', **kwargs) +
+ eg.wim_actions('DELETE', status='DONE', **kwargs))
+ for i, action in enumerate(actions):
+ action['task_index'] = i
+
+ self.populate([
+ {'nfvo_tenants': eg.tenant()}
+ ] + eg.wim_set() + [
+ {'instance_actions': eg.instance_action(**kwargs)},
+ {'vim_wim_actions': actions}
+ ])
+
+ # When we reload the tasks
+ self.thread.reload_actions()
+ # No pending task should be found
+ self.assertEqual(self.thread.pending_tasks, [])
+
+ def test_reload_actions__batch(self):
+ # Given the group_limit is 10, and we have 24
+ group_limit = 10
+ kwargs = {'action_id': uuid('action0')}
+ actions = (eg.wim_actions('CREATE', num_links=8, **kwargs) +
+ eg.wim_actions('FIND', num_links=8, **kwargs) +
+ eg.wim_actions('FIND', num_links=8, **kwargs))
+ for i, action in enumerate(actions):
+ action['task_index'] = i
+
+ self.populate([
+ {'nfvo_tenants': eg.tenant()}
+ ] + eg.wim_set() + [
+ {'instance_actions': eg.instance_action(**kwargs)},
+ {'vim_wim_actions': actions}
+ ])
+
+ # When we reload the tasks
+ self.thread.reload_actions(group_limit)
+
+ # Then we should still see the actions in memory properly
+ self.assertTasksEqual(self.thread.pending_tasks, actions)
+ self.assertEqual(len(self.thread.grouped_tasks.values()), 8)
+
+ @disable_foreign_keys
+ def test_process_list__refresh(self):
+ update_wan_link = MagicMock(wrap=self.persist.update_wan_link)
+ update_action = MagicMock(wrap=self.persist.update_wan_link)
+ patches = dict(update_wan_link=update_wan_link,
+ update_action=update_action)
+
+ with patch.multiple(self.persist, **patches):
+ # Given we have 2 tasks in the refresh queue
+ kwargs = {'action_id': uuid('action0')}
+ actions = (eg.wim_actions('FIND', 'DONE', **kwargs) +
+ eg.wim_actions('CREATE', 'BUILD', **kwargs))
+ for i, action in enumerate(actions):
+ action['task_index'] = i
+
+ self.populate(
+ [{'instance_wim_nets': eg.instance_wim_nets()}] +
+ [{'instance_actions':
+ eg.instance_action(num_tasks=2, **kwargs)}] +
+ [{'vim_wim_actions': actions}])
+
+ self.thread.insert_pending_tasks(actions)
+
+ # When we process the refresh list
+ processed = self.thread.process_list('refresh')
+
+ # Then we should have 2 updates
+ self.assertEqual(processed, 2)
+
+ # And the database should be updated accordingly
+ self.assertEqual(update_wan_link.call_count, 2)
+ self.assertEqual(update_action.call_count, 2)
+
+ @disable_foreign_keys
+ def test_delete_superseed_create(self):
+ # Given we insert a scheduled CREATE task
+ instance_action = eg.instance_action(num_tasks=1)
+ self.thread.pending_tasks = []
+ engine = WimEngine(persistence=self.persist)
+ self.addCleanup(engine.stop_threads)
+ wan_links = eg.instance_wim_nets()
+ create_actions = engine.create_actions(wan_links)
+ delete_actions = engine.delete_actions(wan_links)
+ engine.incorporate_actions(create_actions + delete_actions,
+ instance_action)
+
+ self.populate(instance_actions=instance_action,
+ vim_wim_actions=create_actions + delete_actions)
+
+ self.thread.insert_pending_tasks(create_actions)
+
+ assert self.thread.pending_tasks[0].is_scheduled
+
+ # When we insert the equivalent DELETE task
+ self.thread.insert_pending_tasks(delete_actions)
+
+ # Then the CREATE task should be superseded
+ self.assertEqual(self.thread.pending_tasks[0].action, 'CREATE')
+ assert self.thread.pending_tasks[0].is_superseded
+
+ self.thread.process_list('pending')
+ self.thread.process_list('refresh')
+ self.assertFalse(self.thread.pending_tasks)
+
+
+@ignore_connector
+class TestWimThread(unittest.TestCase):
+ def setUp(self):
+ wim = eg.wim(0)
+ account = eg.wim_account(0, 0)
+ account['wim'] = wim
+ self.persist = MagicMock()
+ self.thread = WimThread(self.persist, account)
+ self.thread.connector = MagicMock()
+
+ super(TestWimThread, self).setUp()
+
+ def test_process_refresh(self):
+ # Given we have 30 tasks in the refresh queue
+ kwargs = {'action_id': uuid('action0')}
+ actions = eg.wim_actions('FIND', 'DONE', num_links=30, **kwargs)
+ self.thread.insert_pending_tasks(actions)
+
+ # When we process the refresh list
+ processed = self.thread.process_list('refresh')
+
+ # Then we should have REFRESH_BATCH updates
+ self.assertEqual(processed, self.thread.BATCH)
+
+ def test_process_refresh__with_superseded(self):
+ # Given we have 30 tasks but 15 of them are superseded
+ kwargs = {'action_id': uuid('action0')}
+ actions = eg.wim_actions('FIND', 'DONE', num_links=30, **kwargs)
+ self.thread.insert_pending_tasks(actions)
+ for task in self.thread.refresh_tasks[0:30:2]:
+ task.status = 'SUPERSEDED'
+
+ now = time()
+
+ # When we call the refresh_elements
+ processed = self.thread.process_list('refresh')
+
+ # Then we should have 25 updates (since SUPERSEDED updates are cheap,
+ # they are not counted for the limits)
+ self.assertEqual(processed, 25)
+
+ # The SUPERSEDED tasks should be removed, 5 tasks should be untouched,
+ # and 10 tasks should be rescheduled
+ refresh_tasks = self.thread.refresh_tasks
+ old = [t for t in refresh_tasks if t.process_at <= now]
+ new = [t for t in refresh_tasks if t.process_at > now]
+ self.assertEqual(len(old), 5)
+ self.assertEqual(len(new), 10)
+ self.assertEqual(len(self.thread.refresh_tasks), 15)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/osm_ro/wim/tox.ini b/osm_ro/wim/tox.ini
new file mode 100644
index 0000000..29f1a8f
--- /dev/null
+++ b/osm_ro/wim/tox.ini
@@ -0,0 +1,58 @@
+# This tox file allows the devs to run unit tests only for this subpackage.
+# In order to do so, cd into the directory and run `tox`
+
+[tox]
+minversion = 1.8
+envlist = py27,flake8,radon
+skipsdist = True
+
+[testenv]
+passenv = *_DB_*
+setenv =
+ PATH = {env:PATH}:{toxinidir}/../../database_utils
+ DBUTILS = {toxinidir}/../../database_utils
+changedir = {toxinidir}
+commands =
+ nosetests -v -d {posargs:tests}
+deps =
+ WebTest
+ logging
+ bottle
+ coverage
+ jsonschema
+ mock
+ mysqlclient
+ nose
+ six
+ PyYaml
+ paramiko
+ ipdb
+ requests
+
+[testenv:flake8]
+changedir = {toxinidir}
+deps = flake8
+commands = flake8 {posargs:.}
+
+[testenv:radon]
+changedir = {toxinidir}
+deps = radon
+commands =
+ radon cc --show-complexity --total-average {posargs:.}
+ radon mi -s {posargs:.}
+
+[coverage:run]
+branch = True
+source = {toxinidir}
+omit =
+ tests
+ tests/*
+ */test_*
+ .tox/*
+
+[coverage:report]
+show_missing = True
+
+[flake8]
+exclude =
+ .tox
diff --git a/osm_ro/wim/wan_link_actions.py b/osm_ro/wim/wan_link_actions.py
new file mode 100644
index 0000000..61c6dd9
--- /dev/null
+++ b/osm_ro/wim/wan_link_actions.py
@@ -0,0 +1,333 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+# pylint: disable=E1101,E0203,W0201
+import json
+from time import time
+
+from ..utils import filter_dict_keys as filter_keys
+from ..utils import merge_dicts, remove_none_items, safe_get, truncate
+from .actions import CreateAction, DeleteAction, FindAction
+from .errors import (
+ InconsistentState,
+ MultipleRecordsFound,
+ NoRecordFound,
+)
+from wimconn import WimConnectorError
+
+INSTANCE_NET_STATUS_ERROR = ('DOWN', 'ERROR', 'VIM_ERROR',
+ 'DELETED', 'SCHEDULED_DELETION')
+INSTANCE_NET_STATUS_PENDING = ('BUILD', 'INACTIVE', 'SCHEDULED_CREATION')
+INSTANCE_VM_STATUS_ERROR = ('ERROR', 'VIM_ERROR',
+ 'DELETED', 'SCHEDULED_DELETION')
+
+
+class RefreshMixin(object):
+ def refresh(self, connector, persistence):
+ """Ask the external WAN Infrastructure Manager system for updates on
+ the status of the task.
+
+ Arguments:
+ connector: object with API for accessing the WAN
+ Infrastructure Manager system
+ persistence: abstraction layer for the database
+ """
+ fields = ('wim_status', 'wim_info', 'error_msg')
+ result = dict.fromkeys(fields)
+
+ try:
+ result.update(
+ connector
+ .get_connectivity_service_status(self.wim_internal_id))
+ except WimConnectorError as ex:
+ self.logger.exception(ex)
+ result.update(wim_status='WIM_ERROR', error_msg=truncate(ex))
+
+ result = filter_keys(result, fields)
+
+ action_changes = remove_none_items({
+ 'extra': merge_dicts(self.extra, result),
+ 'status': 'BUILD' if result['wim_status'] == 'BUILD' else None,
+ 'error_msg': result['error_msg'],
+ 'modified_at': time()})
+ link_changes = merge_dicts(result, status=result.pop('wim_status'))
+ # ^ Rename field: wim_status => status
+
+ persistence.update_wan_link(self.item_id,
+ remove_none_items(link_changes))
+
+ self.save(persistence, **action_changes)
+
+ return result
+
+
+class WanLinkCreate(RefreshMixin, CreateAction):
+ def fail(self, persistence, reason, status='FAILED'):
+ changes = {'status': 'ERROR', 'error_msg': truncate(reason)}
+ persistence.update_wan_link(self.item_id, changes)
+ return super(WanLinkCreate, self).fail(persistence, reason, status)
+
+ def process(self, connector, persistence, ovim):
+ """Process the current task.
+ First we check if all the dependencies are ready,
+ then we call ``execute`` to actually execute the action.
+
+ Arguments:
+ connector: object with API for accessing the WAN
+ Infrastructure Manager system
+ persistence: abstraction layer for the database
+ ovim: instance of openvim, abstraction layer that enable
+ SDN-related operations
+ """
+ wan_link = persistence.get_by_uuid('instance_wim_nets', self.item_id)
+
+ # First we check if all the dependencies are solved
+ instance_nets = persistence.get_instance_nets(
+ wan_link['instance_scenario_id'], wan_link['sce_net_id'])
+
+ try:
+ dependency_statuses = [n['status'] for n in instance_nets]
+ except KeyError:
+ self.logger.debug('`status` not found in\n\n%s\n\n',
+ json.dumps(instance_nets, indent=4))
+ errored = [instance_nets[i]
+ for i, status in enumerate(dependency_statuses)
+ if status in INSTANCE_NET_STATUS_ERROR]
+ if errored:
+ return self.fail(
+ persistence,
+ 'Impossible to stablish WAN connectivity due to an issue '
+ 'with the local networks:\n\t' +
+ '\n\t'.join('{uuid}: {status}'.format(**n) for n in errored))
+
+ pending = [instance_nets[i]
+ for i, status in enumerate(dependency_statuses)
+ if status in INSTANCE_NET_STATUS_PENDING]
+ if pending:
+ return self.defer(
+ persistence,
+ 'Still waiting for the local networks to be active:\n\t' +
+ '\n\t'.join('{uuid}: {status}'.format(**n) for n in pending))
+
+ return self.execute(connector, persistence, ovim, instance_nets)
+
+ def _get_connection_point_info(self, persistence, ovim, instance_net):
+ """Retrieve information about the connection PoP <> WAN
+
+ Arguments:
+ persistence: object that encapsulates persistence logic
+ (e.g. db connection)
+ ovim: object that encapsulates network management logic (openvim)
+ instance_net: record with the information about a local network
+ (inside a VIM). This network will be connected via a WAN link
+ to a different network in a distinct VIM.
+ This method is used to trace what would be the way this network
+ can be accessed from the outside world.
+
+ Returns:
+ dict: Record representing the wan_port_mapping associated to the
+ given instance_net. The expected fields are:
+ **wim_id**, **datacenter_id**, **pop_switch_id** (the local
+ network is expected to be connected at this switch),
+ **pop_switch_port**, **wan_service_endpoint_id**,
+ **wan_service_mapping_info**.
+ """
+ wim_account = persistence.get_wim_account_by(uuid=self.wim_account_id)
+
+ # TODO: make more generic to support networks that are not created with
+ # the SDN assist. This method should have a consistent way of getting
+ # the endpoint for all different types of networks used in the VIM
+ # (provider networks, SDN assist, overlay networks, ...)
+ if instance_net.get('sdn_net_id'):
+ return self._get_connection_point_info_sdn(
+ persistence, ovim, instance_net, wim_account['wim_id'])
+ else:
+ raise InconsistentState(
+ 'The field `instance_nets.sdn_net_id` was expected to be '
+ 'found in the database for the record %s after the network '
+ 'become active, but it is still NULL', instance_net['uuid'])
+
+ def _get_connection_point_info_sdn(self, persistence, ovim,
+ instance_net, wim_id):
+ criteria = {'net_id': instance_net['sdn_net_id']}
+ local_port_mapping = ovim.get_ports(filter=criteria)
+
+ if len(local_port_mapping) > 1:
+ raise MultipleRecordsFound(criteria, 'ovim.ports')
+ local_port_mapping = local_port_mapping[0]
+
+ criteria = {
+ 'wim_id': wim_id,
+ 'pop_switch_dpid': local_port_mapping['switch_dpid'],
+ 'pop_switch_port': local_port_mapping['switch_port'],
+ 'datacenter_id': instance_net['datacenter_id']}
+
+ wan_port_mapping = persistence.query_one(
+ FROM='wim_port_mappings',
+ WHERE=criteria)
+
+ if local_port_mapping.get('vlan'):
+ wan_port_mapping['wan_service_mapping_info']['vlan'] = (
+ local_port_mapping['vlan'])
+
+ return wan_port_mapping
+
+ @staticmethod
+ def _derive_connection_point(wan_info):
+ point = {'service_endpoint_id': wan_info['wan_service_endpoint_id']}
+ # TODO: Cover other scenarios, e.g. VXLAN.
+ details = wan_info.get('wan_service_mapping_info', {})
+ if 'vlan' in details:
+ point['service_endpoint_encapsulation_type'] = 'dot1q'
+ point['service_endpoint_encapsulation_info'] = {
+ 'vlan': details['vlan']
+ }
+ else:
+ point['service_endpoint_encapsulation_type'] = 'none'
+ return point
+
+ @staticmethod
+ def _derive_service_type(connection_points):
+ # TODO: add multipoint and L3 connectivity.
+ if len(connection_points) == 2:
+ return 'ELINE'
+ else:
+ raise NotImplementedError('Multipoint connectivity is not '
+ 'supported yet.')
+
+ def _update_persistent_data(self, persistence, service_uuid, conn_info):
+ """Store plugin/connector specific information in the database"""
+ persistence.update_wan_link(self.item_id, {
+ 'wim_internal_id': service_uuid,
+ 'wim_info': {'conn_info': conn_info},
+ 'status': 'BUILD'})
+
+ def execute(self, connector, persistence, ovim, instance_nets):
+ """Actually execute the action, since now we are sure all the
+ dependencies are solved
+ """
+ try:
+ wan_info = (self._get_connection_point_info(persistence, ovim, net)
+ for net in instance_nets)
+ connection_points = [self._derive_connection_point(w)
+ for w in wan_info]
+
+ uuid, info = connector.create_connectivity_service(
+ self._derive_service_type(connection_points),
+ connection_points
+ # TODO: other properties, e.g. bandwidth
+ )
+ except (WimConnectorError, InconsistentState) as ex:
+ self.logger.exception(ex)
+ return self.fail(
+ persistence,
+ 'Impossible to stablish WAN connectivity.\n\t{}'.format(ex))
+
+ self.logger.debug('WAN connectivity established %s\n%s\n',
+ uuid, json.dumps(info, indent=4))
+ self.wim_internal_id = uuid
+ self._update_persistent_data(persistence, uuid, info)
+ self.succeed(persistence)
+ return uuid
+
+
+class WanLinkDelete(DeleteAction):
+ def succeed(self, persistence):
+ try:
+ persistence.update_wan_link(self.item_id, {'status': 'DELETED'})
+ except NoRecordFound:
+ self.logger.debug('%s(%s) record already deleted',
+ self.item, self.item_id)
+
+ return super(WanLinkDelete, self).succeed(persistence)
+
+ def get_wan_link(self, persistence):
+ """Retrieve information about the wan_link
+
+ It might be cached, or arrive from the database
+ """
+ if self.extra.get('wan_link'):
+ # First try a cached version of the data
+ return self.extra['wan_link']
+
+ return persistence.get_by_uuid(
+ 'instance_wim_nets', self.item_id)
+
+ def process(self, connector, persistence, ovim):
+ """Delete a WAN link previously created"""
+ wan_link = self.get_wan_link(persistence)
+ if 'ERROR' in (wan_link.get('status') or ''):
+ return self.fail(
+ persistence,
+ 'Impossible to delete WAN connectivity, '
+ 'it was never successfully established:'
+ '\n\t{}'.format(wan_link['error_msg']))
+
+ internal_id = wan_link.get('wim_internal_id') or self.internal_id
+
+ if not internal_id:
+ self.logger.debug('No wim_internal_id found in\n%s\n%s\n'
+ 'Assuming no network was created yet, '
+ 'so no network have to be deleted.',
+ json.dumps(wan_link, indent=4),
+ json.dumps(self.as_dict(), indent=4))
+ return self.succeed(persistence)
+
+ try:
+ id = self.wim_internal_id
+ conn_info = safe_get(wan_link, 'wim_info.conn_info')
+ self.logger.debug('Connection Service %s (wan_link: %s):\n%s\n',
+ id, wan_link['uuid'],
+ json.dumps(conn_info, indent=4))
+ result = connector.delete_connectivity_service(id, conn_info)
+ except (WimConnectorError, InconsistentState) as ex:
+ self.logger.exception(ex)
+ return self.fail(
+ persistence,
+ 'Impossible to delete WAN connectivity.\n\t{}'.format(ex))
+
+ self.logger.debug('WAN connectivity removed %s', result)
+ self.succeed(persistence)
+
+ return result
+
+
+class WanLinkFind(RefreshMixin, FindAction):
+ pass
+
+
+ACTIONS = {
+ 'CREATE': WanLinkCreate,
+ 'DELETE': WanLinkDelete,
+ 'FIND': WanLinkFind,
+}
diff --git a/osm_ro/wim/wim_thread.py b/osm_ro/wim/wim_thread.py
new file mode 100644
index 0000000..a13d6a2
--- /dev/null
+++ b/osm_ro/wim/wim_thread.py
@@ -0,0 +1,437 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+
+"""
+Thread-based interaction with WIMs. Tasks are stored in the
+database (vim_wim_actions table) and processed sequentially
+
+Please check the Action class for information about the content of each action.
+"""
+
+import logging
+import threading
+from contextlib import contextmanager
+from functools import partial
+from itertools import islice, chain, takewhile
+from operator import itemgetter, attrgetter
+from sys import exc_info
+from time import time, sleep
+
+from six import reraise
+from six.moves import queue
+
+from . import wan_link_actions, wimconn_odl # wimconn_tapi
+from ..utils import ensure, partition, pipe
+from .actions import IGNORE, PENDING, REFRESH
+from .errors import (
+ DbBaseException,
+ QueueFull,
+ InvalidParameters as Invalid,
+ UndefinedAction,
+)
+from .failing_connector import FailingConnector
+from .wimconn import WimConnectorError
+
+ACTIONS = {
+ 'instance_wim_nets': wan_link_actions.ACTIONS
+}
+
+CONNECTORS = {
+ "odl": wimconn_odl.OdlConnector,
+ # "tapi": wimconn_tapi
+ # Add extra connectors here
+}
+
+
+class WimThread(threading.Thread):
+ """Specialized task queue implementation that runs in an isolated thread.
+
+ Objects of this class have a few methods that are intended to be used
+ outside of the thread:
+
+ - start
+ - insert_task
+ - reload
+ - exit
+
+ All the other methods are used internally to manipulate/process the task
+ queue.
+ """
+ RETRY_SCHEDULED = 10 # 10 seconds
+ REFRESH_BUILD = 10 # 10 seconds
+ REFRESH_ACTIVE = 60 # 1 minute
+ BATCH = 10 # 10 actions per round
+ QUEUE_SIZE = 2000
+ RECOVERY_TIME = 5 # Sleep 5s to leave the system some time to recover
+ MAX_RECOVERY_TIME = 180
+ WAITING_TIME = 1 # Wait 1s for taks to arrive, when there are none
+
+ def __init__(self, persistence, wim_account, logger=None, ovim=None):
+ """Init a thread.
+
+ Arguments:
+ persistence: Database abstraction layer
+ wim_account: Record containing wim_account, tenant and wim
+ information.
+ """
+ name = '{}.{}.{}'.format(wim_account['wim']['name'],
+ wim_account['name'], wim_account['uuid'])
+ super(WimThread, self).__init__(name=name)
+
+ self.name = name
+ self.connector = None
+ self.wim_account = wim_account
+
+ self.logger = logger or logging.getLogger('openmano.wim.'+self.name)
+ self.persist = persistence
+ self.ovim = ovim
+
+ self.task_queue = queue.Queue(self.QUEUE_SIZE)
+
+ self.refresh_tasks = []
+ """Time ordered task list for refreshing the status of WIM nets"""
+
+ self.pending_tasks = []
+ """Time ordered task list for creation, deletion of WIM nets"""
+
+ self.grouped_tasks = {}
+ """ It contains all the creation/deletion pending tasks grouped by
+ its concrete vm, net, etc
+
+ <item><item_id>:
+ - <task1> # e.g. CREATE task
+ <task2> # e.g. DELETE task
+ """
+
+ self._insert_task = {
+ PENDING: partial(self.schedule, list_name='pending'),
+ REFRESH: partial(self.schedule, list_name='refresh'),
+ IGNORE: lambda task, *_, **__: task.save(self.persist)}
+ """Send the task to the right processing queue"""
+
+ def on_start(self):
+ """Run a series of procedures every time the thread (re)starts"""
+ self.connector = self.get_connector()
+ self.reload_actions()
+
+ def get_connector(self):
+ """Create an WimConnector instance according to the wim.type"""
+ error_msg = ''
+ account_id = self.wim_account['uuid']
+ try:
+ account = self.persist.get_wim_account_by(
+ uuid=account_id, hide=None) # Credentials need to be available
+ wim = account['wim']
+ mapping = self.persist.query('wim_port_mappings',
+ WHERE={'wim_id': wim['uuid']},
+ error_if_none=False)
+ return CONNECTORS[wim['type']](wim, account, {
+ 'service_endpoint_mapping': mapping or []
+ })
+ except DbBaseException as ex:
+ error_msg = ('Error when retrieving WIM account ({})\n'
+ .format(account_id)) + str(ex)
+ self.logger.error(error_msg, exc_info=True)
+ except KeyError as ex:
+ error_msg = ('Unable to find the WIM connector for WIM ({})\n'
+ .format(wim['type'])) + str(ex)
+ self.logger.error(error_msg, exc_info=True)
+ except (WimConnectorError, Exception) as ex:
+ # TODO: Remove the Exception class here when the connector class is
+ # ready
+ error_msg = ('Error when loading WIM connector for WIM ({})\n'
+ .format(wim['type'])) + str(ex)
+ self.logger.error(error_msg, exc_info=True)
+
+ error_msg_extra = ('Any task targeting WIM account {} ({}) will fail.'
+ .format(account_id, self.wim_account.get('name')))
+ self.logger.warning(error_msg_extra)
+ return FailingConnector(error_msg + '\n' + error_msg_extra)
+
+ @contextmanager
+ def avoid_exceptions(self):
+ """Make a real effort to keep the thread alive, by avoiding the
+ exceptions. They are instead logged as a critical errors.
+ """
+ try:
+ yield
+ except Exception as ex:
+ self.logger.critical("Unexpected exception %s", ex, exc_info=True)
+ sleep(self.RECOVERY_TIME)
+
+ def reload_actions(self, group_limit=100):
+ """Read actions from database and reload them at memory.
+
+ This method will clean and reload the attributes ``refresh_tasks``,
+ ``pending_tasks`` and ``grouped_tasks``
+
+ Attributes:
+ group_limit (int): maximum number of action groups (those that
+ refer to the same ``<item, item_id>``) to be retrieved from the
+ database in each batch.
+ """
+
+ # First we clean the cache to let the garbage collector work
+ self.refresh_tasks = []
+ self.pending_tasks = []
+ self.grouped_tasks = {}
+
+ offset = 0
+
+ while True:
+ # Do things in batches
+ task_groups = self.persist.get_actions_in_groups(
+ self.wim_account['uuid'], item_types=('instance_wim_nets',),
+ group_offset=offset, group_limit=group_limit)
+ offset += (group_limit - 1) # Update for the next batch
+
+ if not task_groups:
+ break
+
+ pending_groups = (g for _, g in task_groups if is_pending_group(g))
+
+ for task_list in pending_groups:
+ with self.avoid_exceptions():
+ self.insert_pending_tasks(filter_pending_tasks(task_list))
+
+ self.logger.debug(
+ 'Reloaded wim actions pending: %d refresh: %d',
+ len(self.pending_tasks), len(self.refresh_tasks))
+
+ def insert_pending_tasks(self, task_list):
+ """Insert task in the list of actions being processed"""
+ task_list = [action_from(task, self.logger) for task in task_list]
+
+ for task in task_list:
+ group = task.group_key
+ self.grouped_tasks.setdefault(group, [])
+ # Each task can try to supersede the other ones,
+ # but just DELETE actions will actually do
+ task.supersede(self.grouped_tasks[group])
+ self.grouped_tasks[group].append(task)
+
+ # We need a separate loop so each task can check all the other
+ # ones before deciding
+ for task in task_list:
+ self._insert_task[task.processing](task)
+ self.logger.debug('Insert WIM task: %s (%s): %s %s',
+ task.id, task.status, task.action, task.item)
+
+ def schedule(self, task, when=None, list_name='pending'):
+ """Insert a task in the correct list, respecting the schedule.
+ The refreshing list is ordered by threshold_time (task.process_at)
+ It is assumed that this is called inside this thread
+
+ Arguments:
+ task (Action): object representing the task.
+ This object must implement the ``process`` method and inherit
+ from the ``Action`` class
+ list_name: either 'refresh' or 'pending'
+ when (float): unix time in seconds since as a float number
+ """
+ processing_list = {'refresh': self.refresh_tasks,
+ 'pending': self.pending_tasks}[list_name]
+
+ when = when or time()
+ task.process_at = when
+
+ schedule = (t.process_at for t in processing_list)
+ index = len(list(takewhile(lambda moment: moment <= when, schedule)))
+
+ processing_list.insert(index, task)
+ self.logger.debug(
+ 'Schedule of %s in "%s" - waiting position: %d (%f)',
+ task.id, list_name, index, task.process_at)
+
+ return task
+
+ def process_list(self, list_name='pending'):
+ """Process actions in batches and reschedule them if necessary"""
+ task_list, handler = {
+ 'refresh': (self.refresh_tasks, self._refresh_single),
+ 'pending': (self.pending_tasks, self._process_single)}[list_name]
+
+ now = time()
+ waiting = ((i, task) for i, task in enumerate(task_list)
+ if task.process_at is None or task.process_at <= now)
+
+ is_superseded = pipe(itemgetter(1), attrgetter('is_superseded'))
+ superseded, active = partition(is_superseded, waiting)
+ superseded = [(i, t.save(self.persist)) for i, t in superseded]
+
+ batch = islice(active, self.BATCH)
+ refreshed = [(i, handler(t)) for i, t in batch]
+
+ # Since pop changes the indexes in the list, we need to do it backwards
+ remove = sorted([i for i, _ in chain(refreshed, superseded)])
+ return len([task_list.pop(i) for i in reversed(remove)])
+
+ def _refresh_single(self, task):
+ """Refresh just a single task, and reschedule it if necessary"""
+ now = time()
+
+ result = task.refresh(self.connector, self.persist)
+ self.logger.debug('Refreshing WIM task: %s (%s): %s %s => %r',
+ task.id, task.status, task.action, task.item, result)
+
+ interval = self.REFRESH_BUILD if task.is_build else self.REFRESH_ACTIVE
+ self.schedule(task, now + interval, 'refresh')
+
+ return result
+
+ def _process_single(self, task):
+ """Process just a single task, and reschedule it if necessary"""
+ now = time()
+
+ result = task.process(self.connector, self.persist, self.ovim)
+ self.logger.debug('Executing WIM task: %s (%s): %s %s => %r',
+ task.id, task.status, task.action, task.item, result)
+
+ if task.action == 'DELETE':
+ del self.grouped_tasks[task.group_key]
+
+ self._insert_task[task.processing](task, now + self.RETRY_SCHEDULED)
+
+ return result
+
+ def insert_task(self, task):
+ """Send a message to the running thread
+
+ This function is supposed to be called outside of the WIM Thread.
+
+ Arguments:
+ task (str or dict): `"exit"`, `"reload"` or dict representing a
+ task. For more information about the fields in task, please
+ check the Action class.
+ """
+ try:
+ self.task_queue.put(task, False)
+ return None
+ except queue.Full:
+ ex = QueueFull(self.name)
+ reraise(ex.__class__, ex, exc_info()[2])
+
+ def reload(self):
+ """Send a message to the running thread to reload itself"""
+ self.insert_task('reload')
+
+ def exit(self):
+ """Send a message to the running thread to kill itself"""
+ self.insert_task('exit')
+
+ def run(self):
+ self.logger.debug('Starting: %s', self.name)
+ recovery_time = 0
+ while True:
+ self.on_start()
+ reload_thread = False
+ self.logger.debug('Reloaded: %s', self.name)
+
+ while True:
+ with self.avoid_exceptions():
+ while not self.task_queue.empty():
+ task = self.task_queue.get()
+ if isinstance(task, dict):
+ self.insert_pending_tasks([task])
+ elif isinstance(task, list):
+ self.insert_pending_tasks(task)
+ elif isinstance(task, str):
+ if task == 'exit':
+ self.logger.debug('Finishing: %s', self.name)
+ return 0
+ elif task == 'reload':
+ reload_thread = True
+ break
+ self.task_queue.task_done()
+
+ if reload_thread:
+ break
+
+ if not(self.process_list('pending') +
+ self.process_list('refresh')):
+ sleep(self.WAITING_TIME)
+
+ if isinstance(self.connector, FailingConnector):
+ # Wait sometime to try instantiating the connector
+ # again and restart
+ # Increase the recovery time if restarting is not
+ # working (up to a limit)
+ recovery_time = min(self.MAX_RECOVERY_TIME,
+ recovery_time + self.RECOVERY_TIME)
+ sleep(recovery_time)
+ break
+ else:
+ recovery_time = 0
+
+ self.logger.debug("Finishing")
+
+
+def is_pending_group(group):
+ return all(task['action'] != 'DELETE' or
+ task['status'] == 'SCHEDULED'
+ for task in group)
+
+
+def filter_pending_tasks(group):
+ return (t for t in group
+ if (t['status'] == 'SCHEDULED' or
+ t['action'] in ('CREATE', 'FIND')))
+
+
+def action_from(record, logger=None, mapping=ACTIONS):
+ """Create an Action object from a action record (dict)
+
+ Arguments:
+ mapping (dict): Nested data structure that maps the relationship
+ between action properties and object constructors. This data
+ structure should be a dict with 2 levels of keys: item type and
+ action type. Example::
+ {'wan_link':
+ {'CREATE': WanLinkCreate}
+ ...}
+ ...}
+ record (dict): action information
+
+ Return:
+ (Action.Base): Object representing the action
+ """
+ ensure('item' in record, Invalid('`record` should contain "item"'))
+ ensure('action' in record, Invalid('`record` should contain "action"'))
+
+ try:
+ factory = mapping[record['item']][record['action']]
+ return factory(record, logger=logger)
+ except KeyError:
+ ex = UndefinedAction(record['item'], record['action'])
+ reraise(ex.__class__, ex, exc_info()[2])
diff --git a/osm_ro/wim/wimconn.py b/osm_ro/wim/wimconn.py
new file mode 100644
index 0000000..92b6db0
--- /dev/null
+++ b/osm_ro/wim/wimconn.py
@@ -0,0 +1,236 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+"""The WIM connector is responsible for establishing wide area network
+connectivity.
+
+It receives information from the WimThread/WAN Actions about the endpoints of
+a link that spans across multiple datacenters and stablish a path between them.
+"""
+import logging
+
+from ..http_tools.errors import HttpMappedError
+
+
+class WimConnectorError(HttpMappedError):
+ """Base Exception for all connector related errors"""
+
+
+class WimConnector(object):
+ """Abstract base class for all the WIM connectors
+
+ Arguments:
+ wim (dict): WIM record, as stored in the database
+ wim_account (dict): WIM account record, as stored in the database
+ config (dict): optional persistent information related to an specific
+ connector. Inside this dict, a special key,
+ ``service_endpoint_mapping`` provides the internal endpoint
+ mapping.
+ logger (logging.Logger): optional logger object. If none is passed
+ ``openmano.wim.wimconn`` is used.
+
+ The arguments of the constructor are converted to object attributes.
+ An extra property, ``service_endpoint_mapping`` is created from ``config``.
+ """
+ def __init__(self, wim, wim_account, config=None, logger=None):
+ self.logger = logger or logging.getLogger('openmano.wim.wimconn')
+
+ self.wim = wim
+ self.wim_account = wim_account
+ self.config = config or {}
+ self.service_endpoint_mapping = (
+ config.get('service_endpoint_mapping', []))
+
+ def check_credentials(self):
+ """Check if the connector itself can access the WIM.
+
+ Raises:
+ WimConnectorError: Issues regarding authorization, access to
+ external URLs, etc are detected.
+ """
+ raise NotImplementedError
+
+ def get_connectivity_service_status(self, service_uuid, conn_info=None):
+ """Monitor the status of the connectivity service established
+
+ Arguments:
+ service_uuid (str): UUID of the connectivity service
+ conn_info (dict or None): Information returned by the connector
+ during the service creation/edition and subsequently stored in
+ the database.
+
+ Returns:
+ dict: JSON/YAML-serializable dict that contains a mandatory key
+ ``wim_status`` associated with one of the following values::
+
+ {'wim_status': 'ACTIVE'}
+ # The service is up and running.
+
+ {'wim_status': 'INACTIVE'}
+ # The service was created, but the connector
+ # cannot determine yet if connectivity exists
+ # (ideally, the caller needs to wait and check again).
+
+ {'wim_status': 'DOWN'}
+ # Connection was previously established,
+ # but an error/failure was detected.
+
+ {'wim_status': 'ERROR'}
+ # An error occurred when trying to create the service/
+ # establish the connectivity.
+
+ {'wim_status': 'BUILD'}
+ # Still trying to create the service, the caller
+ # needs to wait and check again.
+
+ Additionally ``error_msg``(**str**) and ``wim_info``(**dict**)
+ keys can be used to provide additional status explanation or
+ new information available for the connectivity service.
+ """
+ raise NotImplementedError
+
+ def create_connectivity_service(self, service_type, connection_points,
+ **kwargs):
+ """Stablish WAN connectivity between the endpoints
+
+ Arguments:
+ service_type (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2),
+ ``L3``.
+ connection_points (list): each point corresponds to
+ an entry point from the DC to the transport network. One
+ connection point serves to identify the specific access and
+ some other service parameters, such as encapsulation type.
+ Represented by a dict as follows::
+
+ {
+ "service_endpoint_id": ..., (str[uuid])
+ "service_endpoint_encapsulation_type": ...,
+ (enum: none, dot1q, ...)
+ "service_endpoint_encapsulation_info": {
+ ... (dict)
+ "vlan": ..., (int, present if encapsulation is dot1q)
+ "vni": ... (int, present if encapsulation is vxlan),
+ "peers": [(ipv4_1), (ipv4_2)]
+ (present if encapsulation is vxlan)
+ }
+ }
+
+ The service endpoint ID should be previously informed to the WIM
+ engine in the RO when the WIM port mapping is registered.
+
+ Keyword Arguments:
+ bandwidth (int): value in kilobytes
+ latency (int): value in milliseconds
+
+ Other QoS might be passed as keyword arguments.
+
+ Returns:
+ tuple: ``(service_id, conn_info)`` containing:
+ - *service_uuid* (str): UUID of the established connectivity
+ service
+ - *conn_info* (dict or None): Information to be stored at the
+ database (or ``None``). This information will be provided to
+ the :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
+ **MUST** be JSON/YAML-serializable (plain data structures).
+
+ Raises:
+ WimConnectorException: In case of error.
+ """
+ raise NotImplementedError
+
+ def delete_connectivity_service(self, service_uuid, conn_info=None):
+ """Disconnect multi-site endpoints previously connected
+
+ This method should receive as arguments both the UUID and the
+ connection info dict (respectively), as returned by
+ :meth:`~.create_connectivity_service` and
+ :meth:`~.edit_connectivity_service`.
+
+ Arguments:
+ service_uuid (str): UUID of the connectivity service
+ conn_info (dict or None): Information returned by the connector
+ during the service creation and subsequently stored in the
+ database.
+
+ Raises:
+ WimConnectorException: In case of error.
+ """
+ raise NotImplementedError
+
+ def edit_connectivity_service(self, service_uuid, conn_info=None,
+ connection_points=None, **kwargs):
+ """Change an existing connectivity service.
+
+ This method's arguments and return value follow the same convention as
+ :meth:`~.create_connectivity_service`.
+
+ Arguments:
+ service_uuid (str): UUID of the connectivity service.
+ conn_info (dict or None): Information previously stored in the
+ database.
+ connection_points (list): If provided, the old list of connection
+ points will be replaced.
+
+ Returns:
+ dict or None: Information to be updated and stored at the
+ database.
+ When ``None`` is returned, no information should be changed.
+ When an empty dict is returned, the database record will be
+ deleted.
+ **MUST** be JSON/YAML-serializable (plain data structures).
+
+ Raises:
+ WimConnectorException: In case of error.
+ """
+ raise NotImplementedError
+
+ def clear_all_connectivity_services(self):
+ """Delete all WAN Links in a WIM.
+
+ This method is intended for debugging only, and should delete all the
+ connections controlled by the WIM, not only the WIM connections that
+ a specific RO is aware of.
+
+ Raises:
+ WimConnectorException: In case of error.
+ """
+ raise NotImplementedError
+
+ def get_all_active_connectivity_services(self):
+ """Provide information about all active connections provisioned by a
+ WIM.
+
+ Raises:
+ WimConnectorException: In case of error.
+ """
+ raise NotImplementedError
diff --git a/osm_ro/wim/wimconn_odl.py b/osm_ro/wim/wimconn_odl.py
new file mode 100644
index 0000000..2371046
--- /dev/null
+++ b/osm_ro/wim/wimconn_odl.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+##
+# Copyright 2018 University of Bristol - High Performance Networks Research
+# Group
+# All Rights Reserved.
+#
+# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
+# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
+#
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: <highperformance-networks@bristol.ac.uk>
+#
+# Neither the name of the University of Bristol nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# This work has been performed in the context of DCMS UK 5G Testbeds
+# & Trials Programme and in the framework of the Metro-Haul project -
+# funded by the European Commission under Grant number 761727 through the
+# Horizon 2020 and 5G-PPP programmes.
+##
+from .wimconn import WimConnector
+
+
+# TODO: Basically create this file
+
+class OdlConnector(WimConnector):
+ def get_connectivity_service_status(self, link_uuid):
+ raise NotImplementedError
+
+ def create_connectivity_service(self, *args, **kwargs):
+ raise NotImplementedError
+
+ def delete_connectivity_service(self, link_uuid):
+ raise NotImplementedError
diff --git a/scripts/install-openmano.sh b/scripts/install-openmano.sh
index 3b18494..3789992 100755
--- a/scripts/install-openmano.sh
+++ b/scripts/install-openmano.sh
@@ -246,10 +246,10 @@
"#################################################################"
[ "$_DISTRO" == "Ubuntu" ] && install_packages "python-yaml python-bottle python-mysqldb python-jsonschema "\
"python-paramiko python-argcomplete python-requests python-logutils libxml2-dev libxslt-dev python-dev "\
- "python-pip python-crypto"
+ "python-pip python-crypto python-networkx"
[ "$_DISTRO" == "CentOS" -o "$_DISTRO" == "Red" ] && install_packages "PyYAML MySQL-python python-jsonschema "\
"python-paramiko python-argcomplete python-requests python-logutils libxslt-devel libxml2-devel python-devel "\
- "python-pip python-crypto"
+ "python-pip python-crypto python-networkx"
# The only way to install python-bottle on Centos7 is with easy_install or pip
[ "$_DISTRO" == "CentOS" -o "$_DISTRO" == "Red" ] && easy_install -U bottle
diff --git a/setup.py b/setup.py
index ddce5cd..d5cbe79 100755
--- a/setup.py
+++ b/setup.py
@@ -15,6 +15,7 @@
_license = 'Apache 2.0'
_url = 'https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary'
_requirements = [
+ "six", # python 2 x 3 compatibility
"PyYAML",
"bottle",
#"mysqlclient",
diff --git a/stdeb.cfg b/stdeb.cfg
index ea4c8f7..53abd48 100644
--- a/stdeb.cfg
+++ b/stdeb.cfg
@@ -2,5 +2,5 @@
Suite: xenial
XS-Python-Version: >= 2.7
Maintainer: Gerardo Garcia <gerardo.garciadeblas@telefonica.com>
-Depends: python-pip, libmysqlclient-dev, libssl-dev, libffi-dev, python-argcomplete, python-boto, python-bottle, python-jsonschema, python-logutils, python-cinderclient, python-glanceclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, python-mysqldb, python-lib-osm-openvim, python-osm-im
+Depends: python-pip, libmysqlclient-dev, libssl-dev, libffi-dev, python-argcomplete, python-boto, python-bottle, python-jsonschema, python-logutils, python-cinderclient, python-glanceclient, python-keystoneclient, python-neutronclient, python-novaclient, python-openstackclient, python-mysqldb, python-lib-osm-openvim, python-osm-im, python-networkx
diff --git a/test/test_RO.py b/test/test_RO.py
index 4d814e6..8f2a550 100755
--- a/test/test_RO.py
+++ b/test/test_RO.py
@@ -2287,6 +2287,72 @@
return executed, failed
+def test_wim(args):
+ global test_config
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/osm_ro")
+ import openmanoclient
+ executed = 0
+ failed = 0
+ test_config["client"] = openmanoclient.openmanoclient(
+ endpoint_url=args.endpoint_url,
+ tenant_name=args.tenant_name,
+ datacenter_name=args.datacenter,
+ debug=args.debug, logger=test_config["logger_name"])
+ clsmembers = inspect.getmembers(sys.modules[__name__], inspect.isclass)
+ # If only want to obtain a tests list print it and exit
+ if args.list_tests:
+ tests_names = []
+ for cls in clsmembers:
+ if cls[0].startswith('test_WIM'):
+ tests_names.append(cls[0])
+
+ msg = "The 'wim' set tests are:\n\t" + ', '.join(sorted(tests_names)) +\
+ "\nNOTE: The test test_VIM_tenant_operations will fail in case the used datacenter is type OpenStack " \
+ "unless RO has access to the admin endpoint. Therefore this test is excluded by default"
+ print(msg)
+ logger.info(msg)
+ sys.exit(0)
+
+ # Create the list of tests to be run
+ code_based_tests = []
+ if args.tests:
+ for test in args.tests:
+ for t in test.split(','):
+ matches_code_based_tests = [item for item in clsmembers if item[0] == t]
+ if len(matches_code_based_tests) > 0:
+ code_based_tests.append(matches_code_based_tests[0][1])
+ else:
+ logger.critical("Test '{}' is not among the possible ones".format(t))
+ sys.exit(1)
+ if not code_based_tests:
+ # include all tests
+ for cls in clsmembers:
+ # We exclude 'test_VIM_tenant_operations' unless it is specifically requested by the user
+ if cls[0].startswith('test_VIM') and cls[0] != 'test_VIM_tenant_operations':
+ code_based_tests.append(cls[1])
+
+ logger.debug("tests to be executed: {}".format(code_based_tests))
+
+ # TextTestRunner stream is set to /dev/null in order to avoid the method to directly print the result of tests.
+ # This is handled in the tests using logging.
+ stream = open('/dev/null', 'w')
+
+ # Run code based tests
+ basic_tests_suite = unittest.TestSuite()
+ for test in code_based_tests:
+ basic_tests_suite.addTest(unittest.makeSuite(test))
+ result = unittest.TextTestRunner(stream=stream, failfast=failfast).run(basic_tests_suite)
+ executed += result.testsRun
+ failed += len(result.failures) + len(result.errors)
+ if failfast and failed:
+ sys.exit(1)
+ if len(result.failures) > 0:
+ logger.debug("failures : {}".format(result.failures))
+ if len(result.errors) > 0:
+ logger.debug("errors : {}".format(result.errors))
+ return executed, failed
+
+
def test_deploy(args):
global test_config
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/osm_ro")
@@ -2427,6 +2493,19 @@
vimconn_parser.add_argument('-u', '--url', dest='endpoint_url', default='http://localhost:9090/openmano',
help="Set the openmano server url. By default 'http://localhost:9090/openmano'")
+ # WIM test set
+ # -------------------
+ vimconn_parser = subparsers.add_parser('wim', parents=[parent_parser], help="test wim")
+ vimconn_parser.set_defaults(func=test_wim)
+
+ # Mandatory arguments
+ mandatory_arguments = vimconn_parser.add_argument_group('mandatory arguments')
+ mandatory_arguments.add_argument('-d', '--datacenter', required=True, help='Set the datacenter to test')
+
+ # Optional arguments
+ vimconn_parser.add_argument('-u', '--url', dest='endpoint_url', default='http://localhost:9090/openmano',
+ help="Set the openmano server url. By default 'http://localhost:9090/openmano'")
+
argcomplete.autocomplete(parser)
args = parser.parse_args()
# print str(args)
diff --git a/test/test_openmanoclient.py b/test/test_openmanoclient.py
index 8e13d67..6bdd67c 100755
--- a/test/test_openmanoclient.py
+++ b/test/test_openmanoclient.py
@@ -230,7 +230,80 @@
to_delete_list.insert(0,{"item": "datacenter-detach", "function": client.detach_datacenter, "params":{"name": test_datacenter} })
client["datacenter_name"] = test_datacenter
-
+
+ # WIMs
+ print(" {}. TEST create_wim".format(test_number))
+ test_number += 1
+ long_name = _get_random_name(60)
+
+ wim = client.create_wim(name=long_name, wim_url="http://fakeurl/fake")
+ if verbose: print(wim)
+
+ print(" {}. TEST list_wims".format(test_number))
+ test_number += 1
+ wims = client.list_wims(all_tenants=True)
+ if verbose: print(wims)
+
+ print(" {}. TEST list_tenans filter by name".format(test_number))
+ test_number += 1
+ wims_ = client.list_wims(all_tenants=True, name=long_name)
+ if not wims_["wims"]:
+ raise Exception("Text error, no TENANT found with name")
+ if verbose: print(wims_)
+
+ print(" {}. TEST get_wim by UUID".format(test_number))
+ test_number += 1
+ wim = client.get_wim(uuid=wims_["wims"][0]["uuid"], all_tenants=True)
+ if verbose: print(wim)
+
+ print(" {}. TEST delete_wim by name".format(test_number))
+ test_number += 1
+ wim = client.delete_wim(name=long_name)
+ if verbose: print(wim)
+
+ print(" {}. TEST create_wim for remaining tests".format(test_number))
+ test_number += 1
+ test_wim = "test-wim " + \
+ ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for _ in range(40))
+ wim = client.create_wim(name=test_wim, vim_url="http://127.0.0.1:9080/odl")
+ if verbose: print(wim)
+ to_delete_list.insert(0,
+ {
+ "item": "wim", "function": client.delete_wim,
+ "params":
+ {
+ "name": test_wim
+ }
+ })
+
+ test_wim_tenant = "test-wimtenant " + \
+ ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for _ in range(40))
+
+ # print(" {}. TEST datacenter new tenenat".format(test_number))
+ # test_number += 1
+ # test_vim_tenant = "test-vimtenant " + \
+ # ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for _ in range(40))
+ # vim_tenant = client.vim_action("create", "tenants", datacenter_name=test_datacenter, all_tenants=True,
+ # name=test_vim_tenant)
+ # if verbose: print(vim_tenant)
+ # client["datacenter_name"] = test_datacenter
+ # to_delete_list.insert(0, {"item": "vim_tenant",
+ # "function": client.vim_action,
+ # "params": {
+ # "action": "delete",
+ # "item": "tenants",
+ # "datacenter_name": test_datacenter,
+ # "all_tenants": True,
+ # "uuid": vim_tenant["tenant"]["id"]
+ # }
+ # })
+
+ print(" {}. TEST wim attach".format(test_number))
+ test_number += 1
+ wim = client.attach_wim(name=test_wim, wim_tenant_name=test_wim_tenant)
+ if verbose: print(wim)
+ to_delete_list.insert(0, {"item": "wim-detach", "function": client.detach_wim,
+ "params": {"name": test_wim}})
#VIM_ACTIONS
print(" {}. TEST create_VIM_tenant".format(test_number))