v0.4.47 cloud-init for ssh key injection at instance-scenario-create
authortierno <alfonso.tiernosepulveda@telefonica.com>
Wed, 31 Aug 2016 12:19:40 +0000 (14:19 +0200)
committertierno <alfonso.tiernosepulveda@telefonica.com>
Wed, 31 Aug 2016 12:19:40 +0000 (14:19 +0200)
Change-Id: I8e7410b5952ec54f7eeaf49d6c43234a2c8cf4ff
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
12 files changed:
database_utils/migrate_mano_db.sh
httpserver.py
nfvo.py
nfvo_db.py
openmano
openmanod.py
scenarios/examples/complex2.yaml
scenarios/examples/simple.yaml
test/test_vimconn.sh
vimconn.py
vimconn_openstack.py
vimconn_openvim.py

index f7c2b1b..00d6721 100755 (executable)
@@ -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
index 771ff1b..ce164f3 100644 (file)
@@ -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 c33a44b..ecfe03e 100644 (file)
--- 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]
index 127eb98..6e6b494 100644 (file)
@@ -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"\
index e03c1e9..7c3c5ac 100755 (executable)
--- 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)
 
index c2b61a1..d4094ff 100755 (executable)
@@ -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
index 1a418d4..7bb5855 100644 (file)
@@ -39,6 +39,7 @@ scenario:
       -   VNF2vms: out
       -   VNF3:    data1
     default:
+      external:  true
       interfaces: 
       -   VNF2vms: control0
       -   VNF2vms: control1
index 9384df9..0f7d16e 100644 (file)
@@ -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
 
index 133be8b..57be314 100755 (executable)
@@ -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
index b06f04e..36c3b52 100644 (file)
@@ -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
index 0907e66..6e5f007 100644 (file)
@@ -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)
             
             
index a77c5f6..d0549ad 100644 (file)
@@ -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