diff options
Diffstat (limited to 'lib/leap_cli')
| -rw-r--r-- | lib/leap_cli/commands/compile.rb | 1 | ||||
| -rw-r--r-- | lib/leap_cli/commands/deploy.rb | 2 | ||||
| -rw-r--r-- | lib/leap_cli/commands/list.rb | 8 | ||||
| -rw-r--r-- | lib/leap_cli/config.rb | 119 | ||||
| -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 | ||||
| -rw-r--r-- | lib/leap_cli/config_list.rb | 77 | ||||
| -rw-r--r-- | lib/leap_cli/config_manager.rb | 200 | 
11 files changed, 492 insertions, 402 deletions
| diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb index 6b38de5..8764e52 100644 --- a/lib/leap_cli/commands/compile.rb +++ b/lib/leap_cli/commands/compile.rb @@ -4,7 +4,6 @@ module LeapCli      desc 'Compile json files to hiera configs'      command :compile do |c|        c.action do |global_options,options,args| -        manager = ConfigManager.new          manager.load(Path.provider)          Path.ensure_dir(Path.hiera)          manager.export(Path.hiera) diff --git a/lib/leap_cli/commands/deploy.rb b/lib/leap_cli/commands/deploy.rb index 3694a38..9ec984c 100644 --- a/lib/leap_cli/commands/deploy.rb +++ b/lib/leap_cli/commands/deploy.rb @@ -6,7 +6,7 @@ module LeapCli      arg_name '<node filter>'      command :deploy do |c|        c.action do |global_options,options,args| -        nodes = ConfigManager.filter(args) +        nodes = manager.filter(args)          say "Deploying to these nodes: #{nodes.keys.join(', ')}"          if agree "Continue? "            say "deploy not yet implemented" diff --git a/lib/leap_cli/commands/list.rb b/lib/leap_cli/commands/list.rb index a186049..166ed2a 100644 --- a/lib/leap_cli/commands/list.rb +++ b/lib/leap_cli/commands/list.rb @@ -48,11 +48,11 @@ module LeapCli      command :list do |c|        c.action do |global_options,options,args|          if args.any? -          print_config_table(:nodes, ConfigManager.filter(args)) +          print_config_table(:nodes, manager.filter(args))          else -          print_config_table(:services, ConfigManager.services) -          print_config_table(:tags,     ConfigManager.tags) -          print_config_table(:nodes,  ConfigManager.nodes) +          print_config_table(:services, manager.services) +          print_config_table(:tags, manager.tags) +          print_config_table(:nodes, manager.nodes)          end        end      end diff --git a/lib/leap_cli/config.rb b/lib/leap_cli/config.rb deleted file mode 100644 index 44e66be..0000000 --- a/lib/leap_cli/config.rb +++ /dev/null @@ -1,119 +0,0 @@ -module LeapCli -  # -  # This class represents the configuration for a single node, service, or tag. -  # -  class Config < Hash - -    def initialize(config_type, manager) -      @manager = manager -      @type = config_type -    end - -    # -    # 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 =~ /^= (.*)$/ -          value = eval($1) -          self[key] = value -        end -        value -      end -    end - -    # -    # make the type appear to be a normal Hash in yaml. -    # -    def to_yaml_type -     "!map" -    end - -    # -    # just like Hash#to_yaml, but sorted -    # -    def to_yaml(opts = {}) -      YAML::quick_emit(self, opts) do |out| -        out.map(taguri, to_yaml_style) do |map| -          keys.sort.each do |k| -            v = self.fetch(k) -            map.add(k, v) -          end -        end -      end -    end - -    # -    # make obj['name'] available as obj.name -    # -    def method_missing(method, *args, &block) -      if has_key?(method.to_s) -        self[method.to_s] -      else -        super -      end -    end - -    # -    # convert self into a plain hash, but only include the specified keys -    # -    def to_h(*keys) -      keys.map(&:to_s).inject({}) do |hsh, key| -        if has_key?(key) -          hsh[key] = self[key] -        end -        hsh -      end -    end - -    def nodes -      if @type == :node -        @manager.nodes -      else -        @nodes ||= ConfigList.new -      end -    end - -    def services -      if @type == :node -        self['services'] || [] -      else -        @manager.services -      end -    end - -    def tags -      if @type == :node -        self['tags'] || [] -      else -        @manager.tags -      end -    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
\ No newline at end of file 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 diff --git a/lib/leap_cli/config_list.rb b/lib/leap_cli/config_list.rb deleted file mode 100644 index c8ff23b..0000000 --- a/lib/leap_cli/config_list.rb +++ /dev/null @@ -1,77 +0,0 @@ -module LeapCli -  class ConfigList < 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 = ConfigList.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? ConfigList -        self.merge!(config) -      else -        self[config['name']] = config -      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].to_h(*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 diff --git a/lib/leap_cli/config_manager.rb b/lib/leap_cli/config_manager.rb deleted file mode 100644 index d383cc1..0000000 --- a/lib/leap_cli/config_manager.rb +++ /dev/null @@ -1,200 +0,0 @@ -require 'oj' -require 'yaml' - -module LeapCli - -  class ConfigManager - -    attr_reader :services, :tags, :nodes - -    ## -    ## IMPORT EXPORT -    ## - -    # -    # load .json configuration files -    # -    def load(dir) -      @services = load_all_json("#{dir}/services/*.json") -      @tags     = load_all_json("#{dir}/tags/*.json") -      @common   = load_all_json("#{dir}/common.json")['common'] -      @nodes    = load_all_json("#{dir}/nodes/*.json", :node) -      @nodes.each do |name, node| -        apply_inheritance(node) -      end -      @nodes.each do |name, node| -        node.each {|key,value| node[key] } # force evaluation of dynamic values -      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 = ConfigList.new -      filters.each do |filter| -        if filter =~ /^\+/ -          keep_list = nodes_for_filter(filter[1..-1]) -          node_list.delete_if do |name, node| -            if keep_list[name] -              false -            else -              true -            end -          end -        else -          node_list << nodes_for_filter(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 = ConfigList.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 -      return flatten_hash(hash, Config.new(config_type, self)) -    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 -    end - -    # -    # makes this node inherit options from the common, service, and tag json files. -    # -    def apply_inheritance(node) -      new_node = Config.new(:node, self) -      new_node.merge!(@common) -      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_node.merge!(service) -            service.nodes << node # this is odd, but we want the node pointer, not new_node pointer. -          end -        end -      end -      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_node.merge!(tag) -            tag.nodes << node -          end -        end -      end -      new_node.merge!(node) -      node.replace(new_node) -    end - -    def nodes_for_filter(filter) -      if node = self.nodes[filter] -        ConfigList.new(node) -      elsif service = self.services[filter] -        service.nodes -      elsif tag = self.tags[filter] -        tag.nodes -      end -    end - -  end - -end | 
