diff options
| -rw-r--r-- | lib/leap_cli/config/macros.rb | 90 | ||||
| -rw-r--r-- | lib/leap_cli/config/manager.rb | 53 | ||||
| -rw-r--r-- | lib/leap_cli/config/node.rb | 23 | ||||
| -rw-r--r-- | lib/leap_cli/config/object_list.rb | 7 | ||||
| -rw-r--r-- | lib/leap_cli/config/secrets.rb | 5 | ||||
| -rw-r--r-- | lib/leap_cli/version.rb | 2 | 
6 files changed, 174 insertions, 6 deletions
| diff --git a/lib/leap_cli/config/macros.rb b/lib/leap_cli/config/macros.rb index 2c1f1bd..59453b0 100644 --- a/lib/leap_cli/config/macros.rb +++ b/lib/leap_cli/config/macros.rb @@ -38,6 +38,59 @@ module LeapCli; module Config        nodes[:environment => @node.environment]      end +    # +    # returns a list of nodes that match the location name +    # and environment of @node. +    # +    def nodes_near_me +      if @node['location'] && @node['location']['name'] +        nodes_like_me['location.name' => @node.location.name] +      else +        nodes_like_me['location' => nil] +      end +    end + +    # +    # +    # picks a node out from the node list in such a way that: +    # +    # (1) which nodes picked which nodes is saved in secrets.json +    # (2) when other nodes call this macro with the same node list, they are guaranteed to get a different node +    # (3) if all the nodes in the pick_node list have been picked, remaining nodes are distributed randomly. +    # +    # if the node_list is empty, an exception is raised. +    # if node_list size is 1, then that node is returned and nothing is +    # memorized via the secrets.json file. +    # +    # `label` is needed to distinguish between pools of nodes for different purposes. +    # +    # TODO: more evenly balance after all the nodes have been picked. +    # +    def pick_node(label, node_list) +      if node_list.any? +        if node_list.size == 1 +          return node_list.values.first +        else +          secrets_key = "pick_node(:#{label},#{node_list.keys.sort.join(',')})" +          secrets_value = @manager.secrets.retrieve(secrets_key, @node.environment) || {} +          secrets_value[@node.name] ||= begin +            node_to_pick = nil +            node_list.each_node do |node| +              next if secrets_value.values.include?(node.name) +              node_to_pick = node.name +            end +            node_to_pick ||= secrets_value.values.shuffle.first # all picked already, so pick a random one. +            node_to_pick +          end +          picked_node_name = secrets_value[@node.name] +          @manager.secrets.set(secrets_key, secrets_value, @node.environment) +          return node_list[picked_node_name] +        end +      else +        raise ArgumentError.new('pick_node(node_list): node_list cannot be empty') +      end +    end +      ##      ## FILES      ## @@ -167,9 +220,7 @@ module LeapCli; module Config      #      def hostnames(nodes)        @referenced_nodes ||= ObjectList.new -      if nodes.is_a? Config::Object -        nodes = ObjectList.new nodes -      end +      nodes = listify(nodes)        nodes.each_node do |node|          @referenced_nodes[node.name] ||= node        end @@ -248,6 +299,7 @@ module LeapCli; module Config      #      def stunnel_client(node_list, port, options={})        @next_stunnel_port ||= 4000 +      node_list = listify(node_list)        hostnames(node_list) # record the hosts        result = Config::ObjectList.new        node_list.each_node do |node| @@ -420,9 +472,22 @@ module LeapCli; module Config      end      # -    # wrap something that might fail in `try`. e.g. +    # applies a JSON partial to this node +    # +    def apply_partial(partial_path) +      manager.partials(partial_path).each do |partial_data| +        self.deep_merge!(partial_data) +      end +    end + +    # +    # If at first you don't succeed, then it is time to give up.      # -    # "= try{ nodes[:services => 'tor'].first.ip_address } " +    # try{} returns nil if anything in the block throws an exception. +    # +    # You can wrap something that might fail in `try`, like so. +    # +    #   "= try{ nodes[:services => 'tor'].first.ip_address } "      #      def try(&block)        yield @@ -430,5 +495,20 @@ module LeapCli; module Config        nil      end +    private + +    # +    # returns a node list, if argument is not already one +    # +    def listify(node_list) +      if node_list.is_a? Config::ObjectList +        node_list +      elsif node_list.is_a? Config::Object +        Config::ObjectList.new(node_list) +      else +        raise ArgumentError, 'argument must be a node or node list, not a `%s`' % node_list.class, caller +      end +    end +    end  end; end diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb index 00d2f97..7b3fb27 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -107,14 +107,23 @@ module LeapCli            end          end +        # apply inheritance          @nodes.each do |name, node|            Util::assert! name =~ /^[0-9a-z-]+$/, "Illegal character(s) used in node name '#{name}'"            @nodes[name] = apply_inheritance(node)          end +        # remove disabled nodes          unless options[:include_disabled]            remove_disabled_nodes          end + +        # apply control files +        @nodes.each do |name, node| +          control_files(node).each do |file| +            node.instance_eval File.read(file), file, 1 +          end +        end        end        # @@ -255,6 +264,28 @@ module LeapCli          @nodes[node.name] = apply_inheritance!(node)        end +      # +      # returns all the partial data for the specified partial path. +      # partial path is always relative to provider root, but there must be multiple files +      # that match because provider root might be the base provider or the local provider. +      # +      def partials(partial_path) +        @partials ||= {} +        if @partials[partial_path].nil? +          [Path.provider_base, Path.provider].each do |provider_dir| +            path = File.join(provider_dir, partial_path) +            if File.exists?(path) +              @partials[partial_path] ||= [] +              @partials[partial_path] << load_json(path, Config::Object) +            end +          end +          if @partials[partial_path].nil? +            raise RuntimeError, 'no such partial path `%s`' % partial_path, caller +          end +        end +        @partials[partial_path] +      end +        private        def load_all_json(pattern, object_class, options={}) @@ -438,6 +469,28 @@ module LeapCli          # nothing yet.        end +      # +      # returns a list of 'control' files for this node. +      # a control file is like a service or a tag JSON file, but it contains +      # raw ruby code that gets evaluated in the context of the node. +      # Yes, this entirely breaks our functional programming model +      # for JSON generation. +      # +      def control_files(node) +        files = [] +        [Path.provider_base, @provider_dir].each do |provider_dir| +          [['services', :service_config], ['tags', :tag_config]].each do |attribute, path_sym| +            node[attribute].each do |attr_value| +              path = Path.named_path([path_sym, "#{attr_value}.rb"], provider_dir).sub(/\.json$/,'') +              if File.exists?(path) +                files << path +              end +            end +          end +        end +        return files +      end +      end    end  end diff --git a/lib/leap_cli/config/node.rb b/lib/leap_cli/config/node.rb index 740f9bb..30af5d1 100644 --- a/lib/leap_cli/config/node.rb +++ b/lib/leap_cli/config/node.rb @@ -34,6 +34,29 @@ module LeapCli; module Config      end      # +    # Return a hash table representation of ourselves, with the key equal to the @node.name, +    # and the value equal to the fields specified in *keys. +    # +    # Also, the result is flattened to a single hash, so a key of 'a.b' becomes 'a_b' +    # +    # compare to Object#pick(*keys). This method is the sames as Config::ObjectList#pick_fields, +    # but works on a single node. +    # +    # Example: +    # +    #  node.pick('domain.internal') => +    # +    #    { +    #      'node1': { +    #        'domain_internal': 'node1.example.i' +    #      } +    #    } +    # +    def pick_fields(*keys) +      {@node.name => self.pick(*keys)} +    end + +    #      # can be overridden by the platform.      # returns a list of node names that should be tested before this node      # diff --git a/lib/leap_cli/config/object_list.rb b/lib/leap_cli/config/object_list.rb index e975a5f..cd69d9b 100644 --- a/lib/leap_cli/config/object_list.rb +++ b/lib/leap_cli/config/object_list.rb @@ -20,6 +20,8 @@ module LeapCli        # If the key is a hash, we treat it as a condition and filter all the Config::Objects using the condition.        # A new ObjectList is returned.        # +      # If the key is an array, it is treated as an array of node names +      #        # Examples:        #        # nodes['vpn1'] @@ -64,6 +66,11 @@ module LeapCli              end            end            results +        elsif key.is_a? Array +          key.inject(Config::ObjectList.new) do |list, node_name| +            list[node_name] = super(node_name.to_s) +            list +          end          else            super key.to_s          end diff --git a/lib/leap_cli/config/secrets.rb b/lib/leap_cli/config/secrets.rb index 963fd6b..4450b9c 100644 --- a/lib/leap_cli/config/secrets.rb +++ b/lib/leap_cli/config/secrets.rb @@ -13,6 +13,11 @@ module LeapCli; module Config        @discovered_keys = {}      end +     # we can't use fetch() or get(), since those already have special meanings +     def retrieve(key, environment=nil) +       self.fetch(environment||'default', {})[key.to_s] +     end +      def set(key, value, environment=nil)        environment ||= 'default'        key = key.to_s diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb index cf7e35c..df0f87a 100644 --- a/lib/leap_cli/version.rb +++ b/lib/leap_cli/version.rb @@ -1,6 +1,6 @@  module LeapCli    unless defined?(LeapCli::VERSION) -    VERSION = '1.5.6' +    VERSION = '1.5.7'      COMPATIBLE_PLATFORM_VERSION = '0.5.2'..'1.99'      SUMMARY = 'Command line interface to the LEAP platform'      DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.' | 
