require 'resolv'
module G5K
class Tree < Hash
attr_reader :contents, :path
def write(repository, &block)
self[:uid] ||= ""
@contents = {}
@path = File.join(repository, self[:uid]+".json")
self.each do |key, value|
if value.is_a? Folder
value.write(File.join(repository, self[:uid], key.to_s), &block)
@path = File.join(repository, self[:uid], self[:uid]+".json")
else
@contents.merge!(key => value)
end
end
if !@contents.empty? && !@contents[:uid].empty?
yield self
end
end
def valid?
true
# we could do sthg like: Object.const_get(type.class.name).new(hash_values).valid?
end
end
class Folder < Array
attr_reader :path
def write(repository, &block)
@path = repository
self.each do |v|
v.write(repository, &block)
end
yield self
end
def valid?
true
end
end
class Link < Struct.new(:uid, :refer_to)
attr_reader :from, :path
def write(repository)
@from, @path = [refer_to, File.join(repository, "#{uid}.json")]
yield self
end
def valid?
true
end
end
class ReferenceGenerator
attr_reader :data
attr_reader :config
attr_reader :input
def method_missing(method, *args)
@context.recursive_merge!(method.to_sym => args.first)
end
# Remotly execute commands, and retrieve stdout, stderr, exit code and exit signal.
def ssh_exec!(ssh, command)
stdout_data = ""
stderr_data = ""
exit_code = nil
exit_signal = nil
ssh.open_channel do |channel|
channel.exec(command) do |ch, success|
unless success
abort "FAILED: couldn't execute command (ssh.channel.exec)"
end
channel.on_data do |ch,data|
stdout_data+=data
end
channel.on_extended_data do |ch,type,data|
stderr_data+=data
end
channel.on_request("exit-status") do |ch,data|
exit_code = data.read_long
end
channel.on_request("exit-signal") do |ch, data|
exit_signal = data.read_long
end
end
end
ssh.loop
[stdout_data, stderr_data, exit_code, exit_signal]
end
# Get the IP address corresponding to the host fqdn throught ssh channel
def dns_lookup_through_ssh(ssh,fqdn)
results = ssh_exec! ssh, "host #{fqdn}"
if results[2] == 0
results[0].split(" ").reverse[0]
else
fail "Failed to get ip address of '#{fqdn}' : #{results[1]}"
end
end
def dns_lookup(network_address)
Resolv.getaddress(network_address)
end
# Lookup a key in one of the configuration files passed to the generator
#
# Usage:
# lookup('nancy', 'nodes', 'paramount-1', 'property_name')
# or
# lookup('nancy', 'nodes') { |result| result['paramount-1']['property_name'] }
# or
# lookup('nancy') { |result| result['nodes']['paramount-1']['property_name'] }
#
# assuming you passed a nancy.yaml file to the generator
#
def lookup(filename, *keys, &block)
if config.has_key?(filename)
result = config[filename]
if !keys.empty?
while !keys.empty? do
result = result[keys.shift]
break if result.nil?
end
end
if block
block.call(result)
else
result
end
else
raise ArgumentError, "Cannot fetch the values for '#{keys.inspect}' in the input file '#{filename}'. The config files you gave to me are: '#{config.keys.inspect}'."
end
end
# This method is used exclusivly for environments. Example:
# environment 'squeeze-x64-xen-0.8' do
# available_on %w{bordeaux grenoble lille lyon nancy orsay rennes sophia toulouse}
# end
def available_on(sites_uid)
env_uid = @context[:uid]
old_context = @context
@context = @data
sites_uid.each{|site_uid|
site site_uid.to_sym, {:discard_content => true} do
environment "#{env_uid}", :refer_to => "grid5000/environments/#{env_uid}"
end
}
@context = old_context
end
# This doesn't work with Ruby < 1.8.7. Replaced by a call to build_context (see below).
#
# %w{site cluster environment node service}.each do |method|
# define_method(method) do |uid, *options, &block|
# key = method.pluralize.to_sym
# uid = uid.to_s
# options = options.first || Hash.new
# old_context = @context
# @context[key] ||= G5K::Folder.new
# if options.has_key? :refer_to
# @context[key] << G5K::Link.new(uid, options[:refer_to])
# else
# # if the same object already exists, we return it for completion/modification
# if (same_trees = @context[key].select{|tree| tree[:uid] == uid}).size > 0
# @context = same_trees.first
# else
# @context[key] << G5K::Tree.new.replace({:uid => uid, :type => method})
# @context = @context[key].last
# end
# block.call(uid) if block
# end
# @context = old_context
# end
# end
def site(uid, *options, &block)
build_context(:sites, uid, *options, &block)
end
def cluster(uid, *options, &block)
build_context(:clusters, uid, *options, &block)
end
def server(uid, *options, &block)
build_context(:servers, uid, *options, &block)
end
def environment(uid, *options, &block)
build_context(:environments, uid, *options, &block)
end
def network_equipment(uid, *options, &block)
build_context(:network_equipments, uid, *options, &block)
end
def node(uid, *options, &block)
build_context(:nodes, uid, *options, &block)
end
# def pdu(uid, *options, &block)
# build_context(:pdus, uid, *options, &block)
# end
def service(uid, *options, &block)
build_context(:services, uid, *options, &block)
end
def network_equipment(uid, *options, &block)
build_context(:network_equipments, uid, *options, &block)
end
def build_context(key, uid, *options, &block)
type = key.to_s.chop
uid = uid.to_s
options = options.first || Hash.new
old_context = @context
@context[key] ||= G5K::Folder.new
if options.has_key? :refer_to
@context[key] << G5K::Link.new(uid, options[:refer_to])
else
# if the same object already exists, we return it for completion/modification
if (same_trees = @context[key].select{|tree| tree[:uid] == uid}).size > 0
@context = same_trees.first
else
@context[key] << G5K::Tree.new.replace({:uid => uid, :type => type}.merge((options[:discard_content]) ? {:discard_content => true} : {}))
@context = @context[key].last
end
block.call(uid) if block
end
@context = old_context
end
# Initializes a new generator that will generate data files in a hierachical way.
# The root of the tree will be named with the value of data_description[:uid].
def initialize(data_description = {:uid => ""}, options = {:input => {}, :config => {}})
@input = options[:input]
raise(ArgumentError, "INPUT cannot be null or empty.") if input.nil? || input.empty?
@config = options[:config] || {}
@data = G5K::Tree.new.replace(data_description)
@context = @data
end
def generate
input.each do |filename, content|
eval(content)
end
@data
end
def write(repository, options = {:simulate => false})
things_to_create = []
@data.write("/") do |thing_to_create|
things_to_create << thing_to_create if thing_to_create.valid?
end
groups = things_to_create.group_by{|thing| thing.class}
groups.has_key?(G5K::Folder) and groups[G5K::Folder].each do |folder|
full_path = File.join(repository, folder.path)
unless File.exists?(full_path)
puts "Directory to be written = \t#{full_path}"
FileUtils.mkdir_p(full_path) unless options[:simulate]
end
end
groups.has_key?(G5K::Tree) and groups[G5K::Tree].each do |file|
full_path = File.join(repository, file.path)
new_content = JSON.pretty_generate(file.contents)
existing_content = File.exists?(full_path) ? File.open(full_path, "r").read : ""
if new_content.hash != existing_content.hash
unless file.has_key? :discard_content and file[:discard_content]
puts "File to be written = \t#{full_path}"
File.open(full_path, "w+"){ |f| f << new_content } unless options[:simulate]
end
end
end
groups.has_key?(G5K::Link) and groups[G5K::Link].each do |link|
FileUtils.cd(repository) do |dir|
to = File.join(".", link.path)
from = File.join([".."]*(to.count(File::SEPARATOR)-1), "#{link.from}.json")
unless File.exists?(to)
puts "Symbolic link to be written = \t#{to} -> #{from}"
FileUtils.ln_s(from, to, :force => true) unless options[:simulate]
end
end
end
end
end
end