From a4e1a6ed87de04788ec6855fc8a5e722914e4f03 Mon Sep 17 00:00:00 2001 From: tierno Date: Wed, 31 Aug 2016 14:19:40 +0200 Subject: [PATCH] v0.4.47 cloud-init for ssh key injection at instance-scenario-create Change-Id: I8e7410b5952ec54f7eeaf49d6c43234a2c8cf4ff Signed-off-by: tierno --- database_utils/migrate_mano_db.sh | 17 +++++++++++ httpserver.py | 3 +- nfvo.py | 32 ++++++++++++++++++++- nfvo_db.py | 12 +++++++- openmano | 47 ++++++++++++++++++++++++++++++- openmanod.py | 4 +-- scenarios/examples/complex2.yaml | 1 + scenarios/examples/simple.yaml | 1 + test/test_vimconn.sh | 15 ++++++++-- vimconn.py | 6 +++- vimconn_openstack.py | 24 +++++++++++++++- vimconn_openvim.py | 2 +- 12 files changed, 153 insertions(+), 11 deletions(-) diff --git a/database_utils/migrate_mano_db.sh b/database_utils/migrate_mano_db.sh index f7c2b1bb..00d6721c 100755 --- a/database_utils/migrate_mano_db.sh +++ b/database_utils/migrate_mano_db.sh @@ -166,6 +166,7 @@ DATABASE_TARGET_VER_NUM=0 [ $OPENMANO_VER_NUM -ge 4036 ] && DATABASE_TARGET_VER_NUM=10 #0.4.36=> 10 [ $OPENMANO_VER_NUM -ge 4043 ] && DATABASE_TARGET_VER_NUM=11 #0.4.43=> 11 [ $OPENMANO_VER_NUM -ge 4046 ] && DATABASE_TARGET_VER_NUM=12 #0.4.46=> 12 +[ $OPENMANO_VER_NUM -ge 4047 ] && DATABASE_TARGET_VER_NUM=13 #0.4.47=> 13 #TODO ... put next versions here @@ -595,6 +596,22 @@ function downgrade_from_12(){ echo "DELETE FROM schema_version WHERE version_int='12';" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 } +function upgrade_to_13(){ + echo " upgrade database from version 0.12 to version 0.13" + echo " add cloud_config at 'scenarios', 'instance_scenarios'" + echo "ALTER TABLE scenarios ADD COLUMN cloud_config MEDIUMTEXT NULL DEFAULT NULL AFTER descriptor;" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 + echo "ALTER TABLE instance_scenarios ADD COLUMN cloud_config MEDIUMTEXT NULL DEFAULT NULL AFTER modified_at;" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 + echo "INSERT INTO schema_version (version_int, version, openmano_ver, comments, date) VALUES (13, '0.13', '0.4.47', 'insert cloud-config at scenarios,instance_scenarios', '2016-08-30');" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 +} +function downgrade_from_12(){ + echo " downgrade database from version 0.13 to version 0.12" + echo " remove cloud_config at 'scenarios', 'instance_scenarios'" + echo "ALTER TABLE scenarios DROP COLUMN cloud_config;" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 + echo "ALTER TABLE instance_scenarios DROP COLUMN cloud_config;" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 + echo "DELETE FROM schema_version WHERE version_int='13';" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 +} + + function upgrade_to_X(){ echo " change 'datacenter_nets'" echo "ALTER TABLE datacenter_nets ADD COLUMN vim_tenant_id VARCHAR(36) NOT NULL AFTER datacenter_id, DROP INDEX name_datacenter_id, ADD UNIQUE INDEX name_datacenter_id (name, datacenter_id, vim_tenant_id);" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 diff --git a/httpserver.py b/httpserver.py index 771ff1b0..ce164f36 100644 --- a/httpserver.py +++ b/httpserver.py @@ -975,7 +975,8 @@ def http_post_instances(tenant_id): #parse input data http_content,used_schema = format_in( instance_scenario_create_schema_v01) r = utils.remove_extra_items(http_content, used_schema) - if r is not None: print "http_post_instances: Warning: remove extra items ", r + if r is not None: + logger.warning("http_post_instances: Warning: remove extra items %s", str(r)) data = nfvo.create_instance(mydb, tenant_id, http_content["instance"]) return format_out(data) except (nfvo.NfvoException, db_base_Exception) as e: diff --git a/nfvo.py b/nfvo.py index c33a44b0..ecfe03e5 100644 --- a/nfvo.py +++ b/nfvo.py @@ -1316,6 +1316,27 @@ def start_scenario(mydb, tenant_id, scenario_id, instance_scenario_name, instanc #logger.error("start_scenario %s", error_text) raise NfvoException(error_text, e.http_code) + +def unify_cloud_config(cloud_config): + index_to_delete = [] + users = cloud_config.get("users", []) + for index0 in range(0,len(users)): + if index0 in index_to_delete: + continue + for index1 in range(index0+1,len(users)): + if index1 in index_to_delete: + continue + if users[index0]["name"] == users[index1]["name"]: + index_to_delete.append(index1) + for key in users[index1].get("key-pairs",()): + if "key-pairs" not in users[index0]: + users[index0]["key-pairs"] = [key] + elif key not in users[index0]["key-pairs"]: + users[index0]["key-pairs"].append(key) + index_to_delete.sort(reverse=True) + for index in index_to_delete: + del users[index] + def create_instance(mydb, tenant_id, instance_dict): #print "Checking that nfvo_tenant_id exists and getting the VIM URI and the VIM tenant_id" logger.debug("Creating instance...") @@ -1370,6 +1391,15 @@ def create_instance(mydb, tenant_id, instance_dict): break if not found: raise NfvoException("Invalid vnf name '{}' at instance:vnfs".format(descriptor_vnf), HTTP_Bad_Request) + #0.1 parse cloud-config parameters + cloud_config = scenarioDict.get("cloud-config", {}) + if instance_dict.get("cloud-config"): + cloud_config.update( instance_dict["cloud-config"]) + if not cloud_config: + cloud_config = None + else: + scenarioDict["cloud-config"] = cloud_config + unify_cloud_config(cloud_config) #1. Creating new nets (sce_nets) in the VIM" for sce_net in scenarioDict['nets']: @@ -1540,7 +1570,7 @@ def create_instance(mydb, tenant_id, instance_dict): #print "interfaces", yaml.safe_dump(vm['interfaces'], indent=4, default_flow_style=False) #print ">>>>>>>>>>>>>>>>>>>>>>>>>>>" vm_id = myvim.new_vminstance(myVMDict['name'],myVMDict['description'],myVMDict.get('start', None), - myVMDict['imageRef'],myVMDict['flavorRef'],myVMDict['networks']) + myVMDict['imageRef'],myVMDict['flavorRef'],myVMDict['networks'], cloud_config = cloud_config) vm['vim_id'] = vm_id rollbackList.append({'what':'vm','where':'vim','vim_id':datacenter_id,'uuid':vm_id}) #put interface uuid back to scenario[vnfs][vms[[interfaces] diff --git a/nfvo_db.py b/nfvo_db.py index 127eb98c..6e6b4941 100644 --- a/nfvo_db.py +++ b/nfvo_db.py @@ -30,7 +30,7 @@ __date__ ="$28-aug-2014 10:05:01$" import db_base import MySQLdb as mdb import json -#import yaml +import yaml import time @@ -362,6 +362,9 @@ class nfvo_db(db_base.db_base): 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) scenario_dict = rows[0] + if scenario_dict["cloud_config"]: + scenario_dict["cloud-config"] = yaml.load(scenario_dict["cloud_config"]) + del scenario_dict["cloud_config"] #sce_vnfs cmd = "SELECT uuid,name,vnf_id,description FROM sce_vnfs WHERE scenario_id='{}' ORDER BY created_at".format(scenario_dict['uuid']) self.logger.debug(cmd) @@ -494,6 +497,9 @@ class nfvo_db(db_base.db_base): 'scenario_id' : scenarioDict['uuid'], 'datacenter_id': datacenter_id } + if scenarioDict.get("cloud-config"): + 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={} @@ -582,6 +588,7 @@ class nfvo_db(db_base.db_base): cmd = "SELECT inst.uuid as uuid,inst.name as name,inst.scenario_id as scenario_id, datacenter_id" +\ " ,datacenter_tenant_id, s.name as scenario_name,inst.tenant_id as tenant_id" + \ " ,inst.description as description,inst.created_at as created_at" +\ + " ,inst.cloud_config as 'cloud_config'" +\ " FROM instance_scenarios as inst join scenarios as s on inst.scenario_id=s.uuid"+\ " WHERE " + where_text self.logger.debug(cmd) @@ -593,6 +600,9 @@ class nfvo_db(db_base.db_base): elif self.cur.rowcount>1: raise db_base.db_base_Exception("More than one instance found where " + where_text, db_base.HTTP_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,sv.vnf_id as vnf_id,sv.name as vnf_name, sce_vnf_id, datacenter_id, datacenter_tenant_id"\ diff --git a/openmano b/openmano index e03c1e98..7c3c5ac4 100755 --- a/openmano +++ b/openmano @@ -28,7 +28,7 @@ openmano client used to interact with openmano-server (openmanod) ''' __author__="Alfonso Tierno, Gerardo Garcia" __date__ ="$09-oct-2014 09:09:48$" -__version__="0.4.4-r488" +__version__="0.4.5-r489" version_date="Aug 2016" from argcomplete.completers import FilesCompleter @@ -614,6 +614,49 @@ def instance_create(args): if net_scenario not in myInstance["instance"]["networks"]: myInstance["instance"]["networks"][net_scenario] = {} myInstance["instance"]["networks"][net_scenario]["netmap-create"] = net_datacenter + if args.keypair: + if "cloud-config" not in myInstance["instance"]: + myInstance["instance"]["cloud-config"] = {} + cloud_config = myInstance["instance"]["cloud-config"] + for key in args.keypair: + index = key.find(":") + if index<0: + if "key-pairs" not in cloud_config: + cloud_config["key-pairs"] = [] + cloud_config["key-pairs"].append(key) + else: + user = key[:index] + key_ = key[index+1:] + key_list = key_.split(",") + if "users" not in cloud_config: + cloud_config["users"] = [] + cloud_config["users"].append({"name": user, "key-pairs": key_list }) + if args.keypair_auto: + try: + keys=[] + home = os.getenv("HOME") + user = os.getenv("USER") + files = os.listdir(home+'/.ssh') + for file in files: + if file[-4:] == ".pub": + with open(home+'/.ssh/'+file, 'r') as f: + keys.append(f.read()) + if not keys: + print "Cannot obtain any public ssh key from '{}'. Try not using --keymap-auto".format(home+'/.ssh') + return 1 + 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"] + if "key-pairs" not in cloud_config: + cloud_config["key-pairs"] = [] + if user: + if "users" not in cloud_config: + cloud_config["users"] = [] + 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) @@ -1209,6 +1252,8 @@ if __name__=="__main__": instance_scenario_create_parser.add_argument("--datacenter", action="store", help="specifies the datacenter. Needed if several datacenters are available") instance_scenario_create_parser.add_argument("--netmap-use", action="append", type=str, dest="netmap_use", help="indicates a datacenter network to map a scenario network 'scenario-network=datacenter-network'. Can be used several times") instance_scenario_create_parser.add_argument("--netmap-create", action="append", type=str, dest="netmap_create", help="the scenario network must be created at datacenter 'scenario-network[=datacenter-network-name]' . Can be used several times") + instance_scenario_create_parser.add_argument("--keypair", action="append", type=str, dest="keypair", help="public key for ssh access. Format '[user:]key1[,key2...]'. Can be used several times") + instance_scenario_create_parser.add_argument("--keypair-auto", action="store_true", dest="keypair_auto", help="Inject the user ssh-keys found at $HOME/.ssh directory") instance_scenario_create_parser.add_argument("--description", action="store", help="description of the instance") instance_scenario_create_parser.set_defaults(func=instance_create) diff --git a/openmanod.py b/openmanod.py index c2b61a1b..d4094ffb 100755 --- a/openmanod.py +++ b/openmanod.py @@ -33,9 +33,9 @@ It loads the configuration file and launches the http_server thread that will li ''' __author__="Alfonso Tierno, Gerardo Garcia, Pablo Montes" __date__ ="$26-aug-2014 11:09:29$" -__version__="0.4.46-r485" +__version__="0.4.47-r486" version_date="Aug 2016" -database_version="0.12" #expected database schema version +database_version="0.13" #expected database schema version import httpserver import time diff --git a/scenarios/examples/complex2.yaml b/scenarios/examples/complex2.yaml index 1a418d43..7bb58556 100644 --- a/scenarios/examples/complex2.yaml +++ b/scenarios/examples/complex2.yaml @@ -39,6 +39,7 @@ scenario: - VNF2vms: out - VNF3: data1 default: + external: true interfaces: - VNF2vms: control0 - VNF2vms: control1 diff --git a/scenarios/examples/simple.yaml b/scenarios/examples/simple.yaml index 9384df91..0f7d16eb 100644 --- a/scenarios/examples/simple.yaml +++ b/scenarios/examples/simple.yaml @@ -28,6 +28,7 @@ scenario: vnf_name: linux # VNF name as introduced in OPENMANO DB networks: default: # provide a name for this net or connection + external: true interfaces: - linux1: eth0 # Node and its interface diff --git a/test/test_vimconn.sh b/test/test_vimconn.sh index 133be8be..57be314c 100755 --- a/test/test_vimconn.sh +++ b/test/test_vimconn.sh @@ -110,7 +110,7 @@ then echo "Stopping openmano" $DIRscript/service-openmano.sh mano stop echo "Initializing openmano database" - $DIRmano/database_utils/init_mano_db.sh -u mano -p manopw + $DIRmano/database_utils/init_mano_db.sh -u mano -p manopw --createdb echo "Starting openmano" $DIRscript/service-openmano.sh mano start @@ -248,11 +248,22 @@ then ! is_valid_uuid $scenario && echo FAIL && echo " $result" && $_exit 1 echo $scenario done + + #USER_KEY="" + key_param1="" + key_param2="" + #for file_key in ${HOME}/.ssh/*.pub + #do + # [[ -n ${USER_KEY} ]] && USER_KEY="${USER_KEY}," + # USER_KEY="${USER_KEY}$(cat $file_key)" + #done + #[[ -n ${USER_KEY} ]] && key_param1="--keypair=${USER}:${USER_KEY}" && key_param2="--keypair=${USER_KEY}" + key_param1=--keypair-auto for sce in simple complex2 do printf "%-50s" "Deploying scenario '$sce':" - result=`openmano instance-scenario-create --scenario $sce --name ${sce}-instance` + result=`openmano instance-scenario-create --scenario $sce --name ${sce}-instance "$key_param1" "$key_param2"` instance=`echo $result |gawk '{print $1}'` ! is_valid_uuid $instance && echo FAIL && echo " $result" && $_exit 1 echo $instance diff --git a/vimconn.py b/vimconn.py index b06f04ea..36c3b52c 100644 --- a/vimconn.py +++ b/vimconn.py @@ -269,7 +269,7 @@ class vimconnector(): ''' raise vimconnNotImplemented( "Should have implemented this" ) - def new_vminstance(self,name,description,start,image_id,flavor_id,net_list): + def new_vminstance(self,name,description,start,image_id,flavor_id,net_list,cloud_config=None): '''Adds a VM instance to VIM Params: start: indicates if VM must start or boot in pause mode. Ignored @@ -283,6 +283,10 @@ class vimconnector(): use: 'data', 'bridge', 'mgmt' type: 'virtual', 'PF', 'VF', 'VFnotShared' vim_id: filled/added by this function + cloud_config: can be a text script to be passed directly to cloud-init, + or an object to inject users and ssh keys with format: + key-pairs: [] list of keys to install to the default user + users: [{ name, key-pairs: []}] list of users to add with their key-pair #TODO ip, security groups Returns >=0, the instance identifier <0, error_text diff --git a/vimconn_openstack.py b/vimconn_openstack.py index 0907e667..6e5f0077 100644 --- a/vimconn_openstack.py +++ b/vimconn_openstack.py @@ -544,7 +544,7 @@ class vimconnector(vimconn.vimconnector): except (ksExceptions.ClientException, nvExceptions.ClientException, gl1Exceptions.CommunicationError) as e: self._format_exception(e) - def new_vminstance(self,name,description,start,image_id,flavor_id,net_list): + def new_vminstance(self,name,description,start,image_id,flavor_id,net_list,cloud_config=None): '''Adds a VM instance to VIM Params: start: indicates if VM must start or boot in pause mode. Ignored @@ -614,10 +614,32 @@ class vimconnector(vimconn.vimconnector): security_groups = self.config.get('security_groups') if type(security_groups) is str: security_groups = ( security_groups, ) + if isinstance(cloud_config, dict): + userdata="#cloud-config\nusers:\n" + #default user + if "key-pairs" in cloud_config: + userdata += " - default:\n ssh-authorized-keys:\n" + for key in cloud_config["key-pairs"]: + userdata += " - '{key}'\n".format(key=key) + for user in cloud_config.get("users",[]): + userdata += " - name: {name}\n sudo: ALL=(ALL) NOPASSWD:ALL\n".format(name=user["name"]) + if "user-info" in user: + userdata += " gecos: {}'\n".format(user["user-info"]) + if user.get("key-pairs"): + userdata += " ssh-authorized-keys:\n" + for key in user["key-pairs"]: + userdata += " - '{key}'\n".format(key=key) + self.logger.debug("userdata: %s", userdata) + elif isinstance(cloud_config, str): + userdata = cloud_config + else: + userdata=None + server = self.nova.servers.create(name, image_id, flavor_id, nics=net_list_vim, meta=metadata, security_groups = security_groups, availability_zone = self.config.get('availability_zone'), key_name = self.config.get('keypair'), + userdata=userdata ) #, description=description) diff --git a/vimconn_openvim.py b/vimconn_openvim.py index a77c5f63..d0549adf 100644 --- a/vimconn_openvim.py +++ b/vimconn_openvim.py @@ -745,7 +745,7 @@ class vimconnector(vimconn.vimconnector): #print text return -vim_response.status_code,text - def new_vminstance(self,name,description,start,image_id,flavor_id,net_list): + def new_vminstance(self,name,description,start,image_id,flavor_id,net_list, cloud_config=None): '''Adds a VM instance to VIM Params: start: indicates if VM must start or boot in pause mode. Ignored -- 2.17.1