diff options
Diffstat (limited to 'lib/leap_cli/config')
-rw-r--r-- | lib/leap_cli/config/base.rb | 149 | ||||
-rw-r--r-- | lib/leap_cli/config/list.rb | 81 | ||||
-rw-r--r-- | lib/leap_cli/config/manager.rb | 219 | ||||
-rw-r--r-- | lib/leap_cli/config/node.rb | 19 | ||||
-rw-r--r-- | lib/leap_cli/config/tag.rb | 19 |
5 files changed, 487 insertions, 0 deletions
diff --git a/lib/leap_cli/config/base.rb b/lib/leap_cli/config/base.rb new file mode 100644 index 0000000..c7f4bc9 --- /dev/null +++ b/lib/leap_cli/config/base.rb @@ -0,0 +1,149 @@ +module LeapCli + module Config + # + # This class represents the configuration for a single node, service, or tag. + # + class Base < Hash + + def initialize(manager=nil, node=nil) + @manager = manager + @node = node || self + end + + ## + ## FETCHING VALUES + ## + + # + # lazily eval dynamic values when we encounter them. + # + def [](key) + value = fetch(key, nil) + if value.is_a? Array + value + elsif value.nil? + nil + else + if value =~ /^= (.*)$/ + begin + value = eval($1) + self[key] = value + rescue Exception => exc + puts "Eval error in '#{name}'" + puts " string: #{$1}" + puts " error: #{exc}" + end + end + value + end + end + + def name + @node['name'] + end + + # + # make hash addressable like an object (e.g. obj['name'] available as obj.name) + # + def method_missing(method, *args, &block) + method = method.to_s + if self.has_key?(method) + self[method] + elsif @node != self + @node.send(method) # send call up the tree... + else + raise NoMethodError.new(method) + end + end + + # + # a deep (recursive) merge with another hash or node. + # + def deep_merge!(hsh) + hsh.each do |key,new_value| + old_value = self[key] + if old_value.is_a?(Hash) || new_value.is_a?(Hash) + # merge hashes + value = Base.new(@manager, @node) + old_value.is_a?(Hash) ? value.deep_merge!(old_value) : (value[key] = old_value if old_value.any?) + new_value.is_a?(Hash) ? value.deep_merge!(new_value) : (value[key] = new_value if new_value.any?) + elsif old_value.is_a?(Array) || new_value.is_a?(Array) + # merge arrays + value = [] + old_value.is_a?(Array) ? value += old_value : value << old_value + new_value.is_a?(Array) ? value += new_value : value << new_value + value.compact! + elsif new_value.nil? + value = old_value + elsif old_value.nil? + value = new_value + elsif old_value.is_a?(Boolean) && new_value.is_a?(Boolean) + value = new_value + elsif old_value.class != new_value.class + raise 'Type mismatch. Cannot merge %s with %s. Key value is %s, name is %s.' % [old_value.class, new_value.class, key, name] + else + value = new_value + end + self[key] = value + end + self + end + + #def deep_merge!(new_node) + # new_node.each do |key, value| + # if value.is_a? self.class + # value = Base.new(@manager, @node).deep_merge!(value) + # self[key] = new_node[key] + # end + # self + #end + + # + # like a normal deep_merge, but replace any hash it encounters with a Config::Base + # + #def deep_merge(other_hash) + # p [self['name'], other_hash['name']] + # self.merge(other_hash) do |key, oldval, newval| + # oldval = oldval.to_hash if oldval.respond_to?(:to_hash) + # newval = newval.to_hash if newval.respond_to?(:to_hash) + # p key + # p oldval.class + # p newval.class + # if oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' + # oldval.deep_merge(newval) + # elsif newval.class.to_s == 'Hash' + # p key + # Base.new(@manager, node).replace(newval) + # else + # newval + # end + # end + #end + # + #def deep_merge!(other_hash) + # replace(deep_merge(other_hash)) + #end + + private + + ## + ## MACROS + ## these are methods used when eval'ing a value in the .json configuration + ## + + # + # inserts the contents of a file + # + def file(filename) + filepath = Path.find_file(name, filename) + if filepath + File.read(filepath) + else + log0('no such file, "%s"' % filename) + "" + end + end + + end # class + end # module +end # module
\ No newline at end of file diff --git a/lib/leap_cli/config/list.rb b/lib/leap_cli/config/list.rb new file mode 100644 index 0000000..28ef499 --- /dev/null +++ b/lib/leap_cli/config/list.rb @@ -0,0 +1,81 @@ +module LeapCli + module Config + class List < Hash + + def initialize(config=nil) + if config + self << config + end + end + + # + # if the key is a hash, we treat it as a condition and filter all the configs using the condition + # + # for example: + # + # nodes[:public_dns => true] + # + # will return a ConfigList with node configs that have public_dns set to true + # + def [](key) + if key.is_a? Hash + results = List.new + field, match_value = key.to_a.first + field = field.is_a?(Symbol) ? field.to_s : field + match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value + each do |name, config| + value = config[field] + if !value.nil? + if value.is_a? Array + if value.includes?(match_value) + results[name] = config + end + else + if value == match_value + results[name] = config + end + end + end + end + results + else + super + end + end + + def <<(config) + if config.is_a? Config::List + self.deep_merge!(config) + elsif config['name'] + self[config['name']] = config + else + raise ArgumentError.new('argument must be a Config::Base or a Config::List') + end + end + + # + # converts the hash of configs into an array of hashes, with ONLY the specified fields + # + def fields(*fields) + result = [] + keys.sort.each do |name| + result << self[name].pick(*fields) + end + result + end + + # + # like fields(), but returns an array of values instead of an array of hashes. + # + def field(field) + field = field.to_s + result = [] + keys.sort.each do |name| + result << self[name][field] + end + result + end + + end + end +end diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb new file mode 100644 index 0000000..b1ea68c --- /dev/null +++ b/lib/leap_cli/config/manager.rb @@ -0,0 +1,219 @@ +require 'oj' +require 'yaml' + +module LeapCli + module Config + class Manager + + attr_reader :services, :tags, :nodes + + ## + ## IMPORT EXPORT + ## + + # + # load .json configuration files + # + def load(dir) + @services = load_all_json("#{dir}/services/*.json", :tag) + @tags = load_all_json("#{dir}/tags/*.json", :tag) + @common = load_all_json("#{dir}/common.json", :tag)['common'] + @nodes = load_all_json("#{dir}/nodes/*.json", :node) + @nodes.each do |name, node| + @nodes[name] = apply_inheritance(node) + end + end + + # + # save compiled hiera .yaml files + # + def export(dir) + Dir.glob(dir + '/*.yaml').each do |f| + File.unlink(f) + end + @nodes.each do |name, node| + File.open("#{dir}/#{name}.#{node.domain_internal}.yaml", 'w') do |f| + f.write node.to_yaml + end + end + end + + ## + ## FILTERING + ## + + # + # returns a node list consisting only of nodes that satisfy the filter criteria. + # + # filter: condition [condition] [condition] [+condition] + # condition: [node_name | service_name | tag_name] + # + # if conditions is prefixed with +, then it works like an AND. Otherwise, it works like an OR. + # + def filter(filters) + if filters.empty? + return nodes + end + if filters[0] =~ /^\+/ + # don't let the first filter have a + prefix + filters[0] = filters[0][1..-1] + end + + node_list = Config::List.new + filters.each do |filter| + if filter =~ /^\+/ + keep_list = nodes_for_name(filter[1..-1]) + node_list.delete_if do |name, node| + if keep_list[name] + false + else + true + end + end + else + node_list << nodes_for_name(filter) + end + end + return node_list + end + + ## + ## CLASS METHODS + ## + + #def self.manager + # @manager ||= begin + # manager = ConfigManager.new + # manager.load(Path.provider) + # manager + # end + #end + + #def self.filter(filters); manager.filter(filters); end + #def self.nodes; manager.nodes; end + #def self.services; manager.services; end + #def self.tags; manager.tags; end + + private + + def load_all_json(pattern, config_type = :class) + results = Config::List.new + Dir.glob(pattern).each do |filename| + obj = load_json(filename, config_type) + if obj + name = File.basename(filename).sub(/\.json$/,'') + obj['name'] = name + results[name] = obj + end + end + results + end + + def load_json(filename, config_type) + log2 { filename.sub(/^#{Regexp.escape(Path.root)}/,'') } + + # + # read file, strip out comments + # (File.read(filename) would be faster, but we like ability to have comments) + # + buffer = StringIO.new + File.open(filename) do |f| + while (line = f.gets) + next if line =~ /^\s*#/ + buffer << line + end + end + + # parse json, and flatten hash + begin + hash = Oj.load(buffer.string) || {} + rescue SyntaxError => exc + log0 'Error in file "%s":' % filename + log0 exc.to_s + return nil + end + config = config_type == :node ? Node.new(self) : Tag.new(self) + config.deep_merge!(hash) + return config + end + + # + # remove all the nesting from a hash. + # + # def flatten_hash(input = {}, output = {}, options = {}) + # input.each do |key, value| + # key = options[:prefix].nil? ? "#{key}" : "#{options[:prefix]}#{options[:delimiter]||"_"}#{key}" + # if value.is_a? Hash + # flatten_hash(value, output, :prefix => key, :delimiter => options[:delimiter]) + # else + # output[key] = value + # end + # end + # output.replace(input) + # output + # end + + # + # makes this node inherit options from the common, service, and tag json files. + # + # - takes a hash + # - returns a Node object. + # + def apply_inheritance(node) + new_hash = Node.new(self) + #new_node = Node.new(self) + + # inherit from common + new_hash.deep_merge!(@common) + + # inherit from services + if node['services'] + node['services'].sort.each do |node_service| + service = @services[node_service] + if service.nil? + log0('Error in node "%s": the service "%s" does not exist.' % [node['name'], node_service]) + else + new_hash.deep_merge!(service) + service.nodes << new_hash + end + end + end + + # inherit from tags + if node['tags'] + node['tags'].sort.each do |node_tag| + tag = @tags[node_tag] + if tag.nil? + log0('Error in node "%s": the tag "%s" does not exist.' % [node['name'], node_tag]) + else + new_hash.deep_merge!(tag) + tag.nodes << new_hash + end + end + end + + # inherit from node + new_hash.deep_merge!(node) + + # typecast full hash tree to type Node + #new_node.clone_from_plain_hash!(new_hash) + + return new_hash + end + + # + # returns a set of nodes corresponding to a single name, where name could be a node name, service name, or tag name. + # + def nodes_for_name(name) + if node = self.nodes[name] + Config::List.new(node) + elsif service = self.services[name] + service.nodes + elsif tag = self.tags[name] + tag.nodes + end + end + + end + end +end diff --git a/lib/leap_cli/config/node.rb b/lib/leap_cli/config/node.rb new file mode 100644 index 0000000..5389b44 --- /dev/null +++ b/lib/leap_cli/config/node.rb @@ -0,0 +1,19 @@ +module LeapCli + module Config + class Node < Base + + def nodes + @manager.nodes + end + + def services + self['services'] || [] + end + + def tags + self['tags'] || [] + end + + end + end +end diff --git a/lib/leap_cli/config/tag.rb b/lib/leap_cli/config/tag.rb new file mode 100644 index 0000000..25c7246 --- /dev/null +++ b/lib/leap_cli/config/tag.rb @@ -0,0 +1,19 @@ +module LeapCli + module Config + class Tag < Base + + def nodes + @nodes ||= Config::List.new + end + + def services + @manager.services + end + + def tags + @manager.tags + end + + end + end +end |