From 3ddd8e93a161d748e5703b0856cb2eded0dd19c5 Mon Sep 17 00:00:00 2001 From: elijah Date: Wed, 18 Jun 2014 23:59:05 -0700 Subject: added support for 'control files', files like the .json configuration files but contain arbitrary ruby code evaluated in the context of the node. --- lib/leap_cli/config/macros.rb | 90 +++++++++++++++++++++++++++++++++++--- lib/leap_cli/config/manager.rb | 53 ++++++++++++++++++++++ lib/leap_cli/config/node.rb | 23 ++++++++++ lib/leap_cli/config/object_list.rb | 7 +++ lib/leap_cli/config/secrets.rb | 5 +++ lib/leap_cli/version.rb | 2 +- 6 files changed, 174 insertions(+), 6 deletions(-) (limited to 'lib') 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 @@ -33,6 +33,29 @@ module LeapCli; module Config return vagrant_range.include?(ip_address) 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.' -- cgit v1.2.3