aboutsummaryrefslogtreecommitdiff
path: root/lib/leap_cli/config
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2012-10-10 00:09:31 -0700
committerelijah <elijah@riseup.net>2012-10-10 00:09:31 -0700
commit47062a50e9bba238191838a6625b81793afa8472 (patch)
treeb4b9603f37ba79c522edcc7193cc8e360c36f442 /lib/leap_cli/config
parent2a4dffe29da16dec1147a7f20715a1fa657368ac (diff)
downloadleap_cli-47062a50e9bba238191838a6625b81793afa8472.tar.gz
leap_cli-47062a50e9bba238191838a6625b81793afa8472.tar.bz2
hierarchical yaml output.
Diffstat (limited to 'lib/leap_cli/config')
-rw-r--r--lib/leap_cli/config/base.rb149
-rw-r--r--lib/leap_cli/config/list.rb81
-rw-r--r--lib/leap_cli/config/manager.rb219
-rw-r--r--lib/leap_cli/config/node.rb19
-rw-r--r--lib/leap_cli/config/tag.rb19
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