From 27e7f2bb1bf80667efc234ceabedd72583bc5b6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gaidamour?= <jeremie.gaidamour@inria.fr>
Date: Wed, 9 Mar 2016 16:45:06 +0100
Subject: [PATCH] [dev] The oar-properties generator can now add new nodes to
 the OAR configuration

Also:
* Added support for using a vagrant box and setting SSH options
* The script prevents from adding the same host twice
* Refactoring of the NetSSH code
---
 .../oar-properties/lib/lib-oar-properties.rb  | 102 +++++++++++++--
 generators/oar-properties/oar-properties.rb   | 117 +++++++++++-------
 2 files changed, 162 insertions(+), 57 deletions(-)

diff --git a/generators/oar-properties/lib/lib-oar-properties.rb b/generators/oar-properties/lib/lib-oar-properties.rb
index 71dea002fa7..7970eb98dc5 100755
--- a/generators/oar-properties/lib/lib-oar-properties.rb
+++ b/generators/oar-properties/lib/lib-oar-properties.rb
@@ -14,6 +14,7 @@ class MissingProperty <  StandardError; end
 MiB = 1024**2
 
 # Get node properties from the reference repo hash
+# See also: https://www.grid5000.fr/mediawiki/index.php/Reference_Repository
 def get_node_properties(cluster_uid, cluster, node_uid, node)
   h = {} # ouput
 
@@ -117,6 +118,32 @@ def get_nodelist_properties(site_uid, site)
 end
 
 def diff_node_properties(a, b)
+  a ||= {}
+  b ||= {}
+
+  # default OAR at resource creation:
+  #  available_upto: '2147483647'
+  #  besteffort: 'YES'
+  #  core: ~
+  #  cpu: ~
+  #  cpuset: 0
+  #  deploy: 'NO'
+  #  desktop_computing: 'NO'
+  #  drain: 'NO'
+  #  expiry_date: 0
+  #  finaud_decision: 'YES'
+  #  host: ~
+  #  last_available_upto: 0
+  #  last_job_date: 0
+  #  network_address: server
+  #  next_finaud_decision: 'NO'
+  #  next_state: UnChanged
+  #  resource_id: 9
+  #  scheduler_priority: 0
+  #  state: Suspected
+  #  state_num: 3
+  #  suspended_jobs: 'NO'
+  #  type: default
 
   ignore_keys = [
                  "slash_16",
@@ -168,17 +195,47 @@ def diff_node_properties(a, b)
 
 end
 
-#def cmd_set_oarnodesetting(properties)
-#  properties.each
-#end
+def oarcmd_script_header()
+  return <<EOF
+set -eu
+
+echo '================================================================================'
+
+EOF
+end
+
+def oarcmd_create_node_header()
+  return <<EOF
+nodelist=$(oarnodes -l)
+
+list_contains () { 
+    [[ "$1" =~ (^|[[:space:]])"$2"($|[[:space:]]) ]] && return 0 || return 1
+}
+
+EOF
+
+end
+
+def oarcmd_create_node(host, properties, node_hash) # host = grifffon-1.nancy.grid5000.fr; properties, node_hash: input of the reference API for the node
+  node_uid, site_uid, grid_uid = host.split(".")
+  cluster_uid, node_number     = node_uid.split("-")
+
+  command  = "echo; echo 'Adding host #{host}:'\n"
+  command += 'list_contains "$nodelist" "' + host + '" && '
+  command += "echo '=> host already exist'\n"
+  command += 'list_contains "$nodelist" "' + host + '" || '
+  command += "sudo oar_resources_add -a --hosts 1 --host0 #{node_number} --host-prefix #{cluster_uid}- --host-suffix .#{site_uid}.#{grid_uid}.fr --cpus #{node_hash['architecture']['smp_size']} --cores #{properties['cpucore']}"
+  command += ' | sudo bash'
+  
+  return command + "\n"
+end
 
 def oarcmd_set_node_properties(host, properties)
   #return "# #{host}: OK" if properties.size == 0
   return "" if properties.size == 0
 
-  # command = "# #{host}:\n"
-  # command += "#{ENV["SUDO"]} oarnodesetting -h #{host} -p "
-  command = "oarnodesetting -h #{host} -p "
+  command  = "echo; echo 'Setting properties for #{host}:'; echo\n"
+  command += "sudo oarnodesetting -h #{host} -p "
 
   command +=
     properties.to_a.map{ |(k,v)|
@@ -188,32 +245,32 @@ def oarcmd_set_node_properties(host, properties)
     ! v.nil? ? "#{k}=#{v.inspect.gsub("'", "\\'").gsub("\"", "'")}" : nil
   }.compact.join(' -p ')
   
-  return command
+  return command + "\n"
 end
 # '
 
 # Get the OAR properties from the OAR scheduler
 # This is only needed for the -d option
-def oarcmd_get_nodelist_properties(site_uid, filename=nil, sshkeys=[])
+def oarcmd_get_nodelist_properties(site_uid, filename=nil, options)
   oarnodes_yaml = ""
 
   if filename and File.exist?(filename)
     # Read oar properties from file
-    puts "Read 'oarnodes -Y' from #{filename}"
+    puts "Read 'oarnodes -Y' from #{filename}" if options[:verbose]
     oarnodes_yaml = File.open(filename, 'rb') { |f| f.read }
   else
     # Download the oar properties from the oar server
-    puts "Downloading 'oarnodes -Y' from oar.#{site_uid}.g5kadmin ..."
+    puts "Downloading 'oarnodes -Y' from " + options[:ssh][:host].gsub("%s", site_uid) + "..." if options[:verbose]
 
-    Net::SSH.start("oar.#{site_uid}.g5kadmin", 'g5kadmin', :keys => sshkeys) { |ssh|
+    Net::SSH.start(options[:ssh][:host].gsub("%s", site_uid), options[:ssh][:user], options[:ssh][:params]) { |ssh|
       # capture all stderr and stdout output from a remote process
       oarnodes_yaml = ssh.exec!('oarnodes -Y')
     }
-    puts "... done"
+    puts "... done" if options[:verbose]
 
     if filename
       # Cache the file
-      puts "Save 'oarnodes -Y' as #{filename}"
+      puts "Save 'oarnodes -Y' as #{filename}" if options[:verbose]
       File.write(filename, oarnodes_yaml)
     end
   end
@@ -227,3 +284,22 @@ def oarcmd_get_nodelist_properties(site_uid, filename=nil, sshkeys=[])
   return h
 end
 
+
+def ssh_exec(site_uid, cmds, options)
+  # The following is equivalent to : "cat cmds | bash"
+  #res = ""
+  c = Net::SSH.start(options[:ssh][:host].gsub("%s", site_uid), options[:ssh][:user], options[:ssh][:params])
+  c.open_channel { |channel|
+    channel.exec('bash') { |ch, success|
+      channel.on_data { |ch, data|
+        puts data #if options[:verbose] # ssh cmd output
+      }
+      
+      cmds.each { |cmd| 
+        channel.send_data cmd 
+      }
+      channel.eof!
+    }
+  }
+  c.loop
+end
diff --git a/generators/oar-properties/oar-properties.rb b/generators/oar-properties/oar-properties.rb
index 7d52a5e3b60..689689acab9 100755
--- a/generators/oar-properties/oar-properties.rb
+++ b/generators/oar-properties/oar-properties.rb
@@ -18,8 +18,6 @@ require '../lib/input_loader'
 
 options = {}
 options[:sites] = %w{grenoble lille luxembourg lyon nancy nantes reims rennes sophia}
-options[:diff]  = false
-options[:sshkeys]  = []
 
 OptionParser.new do |opts|
   opts.banner = "Usage: oar-properties.rb [options]"
@@ -60,10 +58,23 @@ OptionParser.new do |opts|
     options[:exec] = e
   end
 
-  opts.on('-k', '--ssh-keys k1,k2,k3', Array, 'SSH keys') do |k|
-    options[:sshkeys] = k
+  opts.on('--ssh-keys k1,k2,k3', Array, 'SSH keys') do |k|
+    options[:ssh] ||= {}
+    options[:ssh][:params] ||= {}
+    options[:ssh][:params][:keys] ||= []
+    options[:ssh][:params][:keys] << k
   end
   
+  opts.on('--vagrant', 'This option modifies the SSH parameters to use a vagrant box instead of Grid5000 servers.') do |v|
+    options[:ssh] ||= {}
+    options[:ssh][:host] = '127.0.0.1' unless options[:ssh][:host]
+    options[:ssh][:user] = 'vagrant'   unless options[:ssh][:user]
+    options[:ssh][:params] ||= {}
+    options[:ssh][:params][:keys] ||= []
+    options[:ssh][:params][:keys] << '~/.vagrant.d/insecure_private_key'
+    options[:ssh][:params][:port] = 2222 unless options[:ssh][:params][:port]
+  end
+
   opts.on("-d", "--diff [YAML filename]", 
           "Only generates the minimal list of commands needed to update the site configuration",
           "The optional YAML file is suppose to be the output of the 'oarnodes -Y' command.",
@@ -73,7 +84,7 @@ OptionParser.new do |opts|
     d = true if d == nil
     options[:diff] = d
   end
-    
+  
   ###
 
   opts.separator ""
@@ -91,6 +102,11 @@ OptionParser.new do |opts|
   end
 end.parse!
 
+options[:ssh] ||= {}
+options[:ssh][:host] = 'oar.%s.g5kadmin' unless options[:ssh][:host]
+options[:ssh][:user] = 'g5kadmin'        unless options[:ssh][:user]
+options[:diff] = false unless options[:diff]
+
 puts "Options: #{options}" if options[:verbose]
 
 nodelist_properties = {} # ["ref"]  : properties from the reference-repo
@@ -117,15 +133,15 @@ if options[:diff]
   options[:sites].each { |site_uid| 
     nodelist_properties["oar"][site_uid] = {}
     filename = options[:diff].is_a?(String) ? options[:diff].gsub("%s", site_uid) : nil
-    nodelist_properties["oar"][site_uid] = oarcmd_get_nodelist_properties(site_uid, filename, options[:sshkeys])
+    nodelist_properties["oar"][site_uid] = oarcmd_get_nodelist_properties(site_uid, filename, options)
   }
 end
 
 #
-# Diff
+# Diff (-d option)
 #
 if options[:diff]
-  header ||= false
+  header = false
   prev_diff = {}
   
   nodelist_properties["diff"] = {}
@@ -136,33 +152,36 @@ if options[:diff]
     site_properties.each_filtered_node_uid(options[:clusters], options[:nodes]) { |node_uid, node_properties_ref|
       
       node_properties_oar = nodelist_properties["oar"][site_uid][node_uid]
-      
+
       diff      = diff_node_properties(node_properties_oar, node_properties_ref)
       diff_keys = diff.map{ |hashdiff_array| hashdiff_array[1] }
       
       nodelist_properties["diff"][site_uid][node_uid] = node_properties_ref.select { |key, value| diff_keys.include?(key) }
-      
+
+      info = (nodelist_properties["oar"][site_uid][node_uid] == nil ? " new node !" : "")      
       case options[:verbose]
       when 1
+        puts "#{node_uid}:#{info}" if info != ""
         puts "#{node_uid}: #{diff_keys}"
       when 2
         # Give more details
         # puts "#{node_uid}: #{diff}"
         if !header
-          header=true
+          header = true
           puts "Output format: ['~', 'key', 'old value', 'new value']"
         end
         if diff.size==0
-          puts "  #{node_uid}: OK"
+          puts "  #{node_uid}: OK#{info}"
         elsif diff == prev_diff
-          puts "  #{node_uid}: same as above"
+          puts "  #{node_uid}:#{info} same modifications as above"
         else
-          puts "  #{node_uid}:"
+          puts "  #{node_uid}:#{info}"
           diff.each { |d| puts "    #{d}" } 
         end
         prev_diff = diff
       when 3
         # Even more details
+        puts "#{node_uid}:#{info}" if info != ""
         puts JSON.pretty_generate({node_uid => {"old values" => node_properties_oar, "new values" => node_properties_ref}})
       end
     }
@@ -171,45 +190,55 @@ if options[:diff]
 end # if options[:diff]
 
 #
-# Output commands
+# Build and execute commands
 #
-if options[:output]
+if options[:output] || options[:exec]
   opt = options[:diff] ? 'diff' : 'ref'
   nodelist_properties[opt].each { |site_uid, site_properties| 
     
+    # Init
     options[:output].is_a?(String) ? o = File.open(options[:output].gsub("%s", site_uid),'w') : o = $stdout.dup
+    ssh_cmd = []
 
+    create_node_header = false
+    cmd = []
+    cmd << oarcmd_script_header()
+
+    #
+    # Build and output commands
+    #
     site_properties.each_filtered_node_uid(options[:clusters], options[:nodes]) { |node_uid, node_properties|
-      o.write(oarcmd_set_node_properties(node_uid + "." + site_uid + ".grid5000.fr", node_properties) + "\n")
+      # Create new nodes
+      if (opt == 'ref' || nodelist_properties['oar'][site_uid][node_uid] == nil)
+        if !create_node_header
+          create_node_header = true
+          cmd << oarcmd_create_node_header()
+        end
+
+        cluster_uid = node_uid.split('-')[0]
+        node_hash = global_hash['sites'][site_uid]['clusters'][cluster_uid]['nodes'][node_uid]
+        cmd << oarcmd_create_node(node_uid + '.' + site_uid + '.grid5000.fr', node_properties, node_hash) + "\n"
+      end
+
+      # Update properties
+      cmd << oarcmd_set_node_properties(node_uid + '.' + site_uid + '.grid5000.fr', node_properties) + "\n"
+
+      cmd << "echo '================================================================================'\n\n"
+      ssh_cmd += cmd        if options[:exec]
+      o.write(cmd.join('')) if options[:output]
+      cmd = []
     }
     
     o.close
     
-  }
-end
+    #
+    # Execute commands
+    #
+    if options[:exec]
+      printf "Apply changes to the OAR server " + options[:ssh][:host].gsub("%s", site_uid) + " ? (y/N) "
+      prompt = STDIN.gets.chomp
+      ssh_exec(site_uid, ssh_cmd, options) if prompt == 'y'
+    end
+  } # site loop
+end # if options[:output] || options[:exec]
 
-#
-# Execute commands
-#
-if options[:exec]
-  printf "Apply changes to the OAR servers ? (y/n)"
-  prompt = STDIN.gets.chomp
-  exit unless prompt == 'y'
-
-  opt = options[:diff] ? 'diff' : 'ref'
-  nodelist_properties[opt].each { |site_uid, site_properties| 
-    
-    puts "Connecting #{site_uid} ..."
-    Net::SSH.start("oar.#{site_uid}.g5kadmin", 'g5kadmin', :keys => options[:sshkeys]) { |ssh|
-    
-      site_properties.each_filtered_node_uid(options[:clusters], options[:nodes]) { |node_uid, node_properties|
-        cmd = oarcmd_set_node_properties(node_uid + "." + site_uid + ".grid5000.fr", node_properties)
-        if cmd.size>0
-          puts "#{cmd}" if options[:verbose]
-          ssh_output = ssh.exec!('sudo ' + cmd) 
-          puts "#{ssh_output}\n" if options[:verbose]
-        end
-      }
-    }
-  }
-end
-- 
GitLab