From ce140bdde2c5c58eab7eba8e8fc9d6e8fe12755f Mon Sep 17 00:00:00 2001 From: elijah Date: Sun, 5 Oct 2014 23:19:24 -0700 Subject: more robust env pinning, fixes several edge case bugs. --- lib/leap_cli.rb | 1 + lib/leap_cli/config/filter.rb | 146 +++++++++++++++++++++++++++++++++++++ lib/leap_cli/config/manager.rb | 61 +--------------- lib/leap_cli/config/object_list.rb | 90 +++++++++++------------ 4 files changed, 195 insertions(+), 103 deletions(-) create mode 100644 lib/leap_cli/config/filter.rb diff --git a/lib/leap_cli.rb b/lib/leap_cli.rb index fbfde59..be16cf4 100644 --- a/lib/leap_cli.rb +++ b/lib/leap_cli.rb @@ -34,6 +34,7 @@ require 'leap_cli/config/tag' require 'leap_cli/config/provider' require 'leap_cli/config/secrets' require 'leap_cli/config/object_list' +require 'leap_cli/config/filter' require 'leap_cli/config/manager' require 'leap_cli/markdown_document_listener' diff --git a/lib/leap_cli/config/filter.rb b/lib/leap_cli/config/filter.rb new file mode 100644 index 0000000..ce218da --- /dev/null +++ b/lib/leap_cli/config/filter.rb @@ -0,0 +1,146 @@ +# +# Many leap_cli commands accept a list of filters to select a subset of nodes for the command to +# be applied to. This class is a helper for manager to run these filters. +# +# Classes other than Manager should not use this class. +# + +module LeapCli + module Config + class Filter + + # + # filter -- array of strings, each one a filter + # options -- hash, possible keys include + # :nopin -- disregard environment pinning + # :local -- if false, disallow local nodes + # + def initialize(filters, options, manager) + @filters = filters.nil? ? [] : filters.dup + @environments = [] + @options = options + @manager = manager + + # split filters by pulling out items that happen + # to be environment names. + if LeapCli.leapfile.environment.nil? || @options[:nopin] + @environments = [] + else + @environments = [LeapCli.leapfile.environment] + end + @filters.select! do |filter| + filter_text = filter.sub(/^\+/,'') + if is_environment?(filter_text) + if filter_text == LeapCli.leapfile.environment + # silently ignore already pinned environments + elsif (filter =~ /^\+/ || @filters.first == filter) && !@environments.empty? + LeapCli::Util.bail! do + LeapCli::Util.log "Environments are exclusive: no node is in two environments." do + LeapCli::Util.log "Tried to filter on '#{@environments.join('\' AND \'')}' AND '#{filter_text}'" + end + end + else + @environments << filter_text + end + false + else + true + end + end + + # don't let the first filter have a + prefix + if @filters[0] =~ /^\+/ + @filters[0] = @filters[0][1..-1] + end + end + + # actually run the filter, returns a filtered list of nodes + def nodes() + if @filters.empty? + return nodes_for_empty_filter + else + return nodes_for_filter + end + end + + private + + def nodes_for_empty_filter + node_list = @manager.nodes + if @environments.any? + node_list = node_list[ @environments.collect{|e|[:environment, env_to_filter(e)]} ] + end + if @options[:local] === false + node_list = node_list[:environment => '!local'] + end + node_list + end + + def nodes_for_filter + node_list = Config::ObjectList.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.merge!(nodes_for_name(filter)) + end + end + node_list + end + + private + + # + # returns a set of nodes corresponding to a single name, + # where name could be a node name, service name, or tag name. + # + # For services and tags, we only include nodes for the + # environments that are active + # + def nodes_for_name(name) + if node = @manager.nodes[name] + return Config::ObjectList.new(node) + elsif @environments.empty? + if @manager.services[name] + @manager.env('_all_').services[name].node_list + elsif @manager.tags[name] + @manager.env('_all_').tags[name].node_list + end + else + node_list = Config::ObjectList.new + if @manager.services[name] + @environments.each do |env| + node_list.merge!(@manager.env(env).services[name].node_list) + end + elsif @manager.tags[name] + @environments.each do |env| + node_list.merge!(@manager.env(env).tags[name].node_list) + end + end + return node_list + end + end + + # + # when pinning, we use the name 'default' to specify nodes + # without an environment set, but when filtering, we need to filter + # on :environment => nil. + # + def env_to_filter(environment) + environment == 'default' ? nil : environment + end + + def is_environment?(text) + text == 'default' || @manager.environment_names.include?(text) + end + + end + end +end diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb index 26d45c3..be95831 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -229,57 +229,19 @@ module LeapCli # 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] + # condition: [node_name | service_name | tag_name | environment_name] # # if conditions is prefixed with +, then it works like an AND. Otherwise, it works like an OR. # - # The environment is pinned, then all filters get an automatic +environment_name - # applied (whatever the name happens to be). + # args: + # filter -- array of filter terms, one per item # # options: # :local -- if :local is false and the filter is empty, then local nodes are excluded. # :nopin -- if true, ignore environment pinning # def filter(filters=nil, options={}) - # handle empty filter - if filters.nil? || filters.empty? - node_list = self.nodes - if LeapCli.leapfile.environment - node_list = node_list[:environment => LeapCli.leapfile.environment_filter] - end - if options[:local] === false - node_list = node_list[:environment => '!local'] - end - return node_list - end - - # handle non-empty filters - if filters[0] =~ /^\+/ - # don't let the first filter have a + prefix - filters[0] = filters[0][1..-1] - end - node_list = Config::ObjectList.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.merge!(nodes_for_name(filter)) - end - end - - # optionally apply environment pin - if !options[:nopin] && LeapCli.leapfile.environment - node_list = node_list[:environment => LeapCli.leapfile.environment_filter] - end - - return node_list + Filter.new(filters, options, self).nodes() end # @@ -512,21 +474,6 @@ module LeapCli end 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::ObjectList.new(node) - elsif service = self.services[name] - service.node_list - elsif tag = self.tags[name] - tag.node_list - else - {} - end - end - def validate_provider(provider) # nothing yet. end diff --git a/lib/leap_cli/config/object_list.rb b/lib/leap_cli/config/object_list.rb index cd69d9b..33ca4dd 100644 --- a/lib/leap_cli/config/object_list.rb +++ b/lib/leap_cli/config/object_list.rb @@ -20,8 +20,6 @@ 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'] @@ -30,47 +28,22 @@ module LeapCli # nodes[:public_dns => true] # all nodes with public dns # - # nodes[:services => 'openvpn', :services => 'tor'] + # nodes[:services => 'openvpn', 'location.country_code' => 'US'] + # all nodes with services containing 'openvpn' OR country code of US + # + # Sometimes, you want to do an OR condition with multiple conditions + # for the same field. Since hash keys must be unique, you can use + # an array representation instead: + # + # nodes[[:services, 'openvpn'], [:services, 'tor']] # nodes with openvpn OR tor service # # nodes[:services => 'openvpn'][:tags => 'production'] # nodes with openvpn AND are production # def [](key) - if key.is_a? Hash - results = Config::ObjectList.new - key.each do |field, match_value| - field = field.is_a?(Symbol) ? field.to_s : field - match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value - if match_value.is_a?(String) && match_value =~ /^!/ - operator = :not_equal - match_value = match_value.sub(/^!/, '') - else - operator = :equal - end - each do |name, config| - value = config[field] - if value.is_a? Array - if operator == :equal && value.include?(match_value) - results[name] = config - elsif operator == :not_equal && !value.include?(match_value) - results[name] = config - end - else - if operator == :equal && value == match_value - results[name] = config - elsif operator == :not_equal && value != match_value - results[name] = config - end - end - 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 + if key.is_a?(Hash) || key.is_a?(Array) + filter(key) else super key.to_s end @@ -88,15 +61,40 @@ module LeapCli end end - # def <<(object) - # if object.is_a? Config::ObjectList - # self.merge!(object) - # elsif object['name'] - # self[object['name']] = object - # else - # raise ArgumentError.new('argument must be a Config::Object or a Config::ObjectList') - # end - # end + # + # filters this object list, producing a new list. + # filter is an array or a hash. see [] + # + def filter(filter) + results = Config::ObjectList.new + filter.each do |field, match_value| + field = field.is_a?(Symbol) ? field.to_s : field + match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value + if match_value.is_a?(String) && match_value =~ /^!/ + operator = :not_equal + match_value = match_value.sub(/^!/, '') + else + operator = :equal + end + each do |name, config| + value = config[field] + if value.is_a? Array + if operator == :equal && value.include?(match_value) + results[name] = config + elsif operator == :not_equal && !value.include?(match_value) + results[name] = config + end + else + if operator == :equal && value == match_value + results[name] = config + elsif operator == :not_equal && value != match_value + results[name] = config + end + end + end + end + results + end def add(name, object) self[name] = object -- cgit v1.2.3