Class: HybridPlatformsConductor::NodesHandler

Inherits:
Object
  • Object
show all
Includes:
LoggerHelpers, ParallelThreads
Defined in:
lib/hybrid_platforms_conductor/nodes_handler.rb

Overview

API to get information on our inventory: nodes and their metadata

Defined Under Namespace

Modules: ConfigDSLExtension

Constant Summary

Constants included from LoggerHelpers

LoggerHelpers::LEVELS_MODIFIERS, LoggerHelpers::LEVELS_TO_STDERR

Instance Method Summary collapse

Methods included from LoggerHelpers

#err, #init_loggers, #log_component=, #log_debug?, #log_level=, #out, #section, #set_loggers_format, #stderr_device, #stderr_device=, #stderr_displayed?, #stdout_device, #stdout_device=, #stdout_displayed?, #stdouts_to_s, #with_progress_bar

Methods included from ParallelThreads

#for_each_element_in

Constructor Details

#initialize(logger: Logger.new(STDOUT), logger_stderr: Logger.new(STDERR), config: Config.new, cmd_runner: CmdRunner.new, platforms_handler: PlatformsHandler.new) ⇒ NodesHandler

Constructor

Parameters
  • logger (Logger): Logger to be used [default: Logger.new(STDOUT)]

  • logger_stderr (Logger): Logger to be used for stderr [default: Logger.new(STDERR)]

  • config (Config): Config to be used. [default: Config.new]

  • cmd_runner (CmdRunner): Command executor to be used. [default: CmdRunner.new]

  • platforms_handler (PlatformsHandler): Platforms Handler to be used. [default: PlatformsHandler.new]



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 51

def initialize(
  logger: Logger.new(STDOUT),
  logger_stderr: Logger.new(STDERR),
  config: Config.new,
  cmd_runner: CmdRunner.new,
  platforms_handler: PlatformsHandler.new
)
  init_loggers(logger, logger_stderr)
  @config = config
  @cmd_runner = cmd_runner
  @platforms_handler = platforms_handler
  # List of platform handler per known node
  # Hash<String, PlatformHandler>
  @nodes_platform = {}
  # List of platform handler per known nodes list
  # Hash<String, PlatformHandler>
  @nodes_list_platform = {}
  # List of CMDBs getting a property, per property name
  # Hash<Symbol, Array<Cmdb> >
  @cmdbs_per_property = {}
  # List of CMDBs having the get_others method
  # Array< Cmdb >
  @cmdbs_others = []
  @cmdbs = Plugins.new(
    :cmdb,
    logger: @logger,
    logger_stderr: @logger_stderr,
    init_plugin: proc do |plugin_class|
      cmdb = plugin_class.new(
        logger: @logger,
        logger_stderr: @logger_stderr,
        config: @config,
        cmd_runner: @cmd_runner,
        platforms_handler: @platforms_handler,
        nodes_handler: self
      )
      @cmdbs_others << cmdb if cmdb.respond_to?(:get_others)
      cmdb.methods.each do |method|
        if method.to_s =~ /^get_(.*)$/
          property = $1.to_sym
          @cmdbs_per_property[property] = [] unless @cmdbs_per_property.key?(property)
          @cmdbs_per_property[property] << cmdb
        end
      end
      cmdb
    end
  )
  # Cache of metadata per node
  # Hash<String, Hash<Symbol, Object> >
  @metadata = {}
  # The metadata update is protected by a mutex to make it thread-safe
  @metadata_mutex = Mutex.new
  # Cache of CMDB masters, per property, per node
  # Hash< String, Hash< Symbol, Cmdb > >
  @cmdb_masters_cache = {}
  # Read all platforms from the config
  @platforms_handler.known_platforms.each do |platform|
    # Register all known nodes for this platform
    platform.known_nodes.each do |node|
      raise "Can't register #{node} to platform #{platform.repository_path}, as it is already defined in platform #{@nodes_platform[node].repository_path}." if @nodes_platform.key?(node)
      @nodes_platform[node] = platform
    end
    # Register all known nodes lists
    platform.known_nodes_lists.each do |nodes_list|
      raise "Can't register nodes list #{nodes_list} to platform #{platform.repository_path}, as it is already defined in platform #{@nodes_list_platform[nodes_list].repository_path}." if @nodes_list_platform.key?(nodes_list)
      @nodes_list_platform[nodes_list] = platform
    end if platform.respond_to?(:known_nodes_lists)
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

Accept any method of name get_<property>_of to get the metadata property of a given node. Here is the magic of accepting method names that are not statically defined.

Parameters
  • method (Symbol): The missing method name

  • args (Array<Object>): Arguments given to the call

  • block (Proc): Code block given to the call



301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 301

def method_missing(method, *args, &block)
  if method.to_s =~ /^get_(.*)_of$/
    property = $1.to_sym
    # Define the method so that we don't go trough method_missing next time (more efficient).
    define_property_method_for(property)
    # Then call it
    send("get_#{property}_of".to_sym, *args, &block)
  else
    # We really don't know this method.
    # Call original implementation of method_missing that will raise an exception.
    super
  end
end

Instance Method Details

#define_property_method_for(property) ⇒ Object

Define a method to get a metadata property of a node. This is like a factory of method shortcuts for properties. The method will be named get_<property>_of. This way instead of calling

 node, :host_ip

we can call

get_host_ip_of node

Readability wins :D

Parameters
  • property (Symbol): The property name



290
291
292
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 290

def define_property_method_for(property)
  define_singleton_method("get_#{property}_of".to_sym) { |node| (node, property) }
end

#for_each_node_in(nodes, parallel: false, nbr_threads_max: nil, progress: 'Processing nodes') ⇒ Object

Iterate over a list of nodes. Provide a mechanism to multithread this iteration (in such case the iterating code has to be thread-safe). In case of multithreaded run, a progress bar is being displayed.

Parameters
  • nodes (Array<String>): List of nodes to iterate over

  • parallel (Boolean): Iterate in a multithreaded way? [default: false]

  • nbr_threads_max (Integer or nil): Maximum number of threads to be used in case of parallel, or nil for no limit [default: nil]

  • progress (String or nil): Name of a progress bar to follow the progression, or nil for no progress bar [default: ‘Processing nodes’]

  • Proc: The code called for each node being iterated on.

    • Parameters
      • node (String): The node name



466
467
468
469
470
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 466

def for_each_node_in(nodes, parallel: false, nbr_threads_max: nil, progress: 'Processing nodes')
  for_each_element_in(nodes.sort, parallel: parallel, nbr_threads_max: nbr_threads_max, progress: progress) do |node|
    yield node
  end
end

#impacted_nodes_from_git_diff(platform_name, from_commit: 'master', to_commit: nil, smallest_set: false) ⇒ Object

Get the list of impacted nodes from a git diff on a platform

Parameters
  • platform_name (String): The platform’s name

  • from_commit (String): Commit ID to check from [default: ‘master’]

  • to_commit (String or nil): Commit ID to check to, or nil for currently checked-out files [default: nil]

  • smallest_set (Boolean): Smallest set of impacted nodes? [default: false]

Result
  • Array<String>: The list of nodes impacted by this diff (counting direct impacts, services and global files impacted)

  • Array<String>: The list of nodes directly impacted by this diff

  • Array<String>: The list of services impacted by this diff

  • Boolean: Are there some files that have a global impact (meaning all nodes are potentially impacted by this diff)?



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 484

def impacted_nodes_from_git_diff(platform_name, from_commit: 'master', to_commit: nil, smallest_set: false)
  platform = @platforms_handler.platform(platform_name)
  raise "Unkown platform #{platform_name}. Possible platforms are #{@platforms_handler.known_platforms.map(&:name).sort.join(', ')}" if platform.nil?
  _exit_status, stdout, _stderr = @cmd_runner.run_cmd "cd #{platform.repository_path} && git --no-pager diff --no-color #{from_commit} #{to_commit.nil? ? '' : to_commit}", log_to_stdout: log_debug?
  # Parse the git diff output to create a structured diff
  # Hash< String, Hash< Symbol, Object > >: List of diffs info, per file name having a diff. Diffs info have the following properties:
  # * *moved_to* (String): The new file path, in case it has been moved [optional]
  # * *diff* (String): The diff content
  files_diffs = {}
  current_file_diff = nil
  stdout.split("\n").each do |line|
    case line
    when /^diff --git a\/(.+) b\/(.+)$/
      # A new file diff
      from, to = $1, $2
      current_file_diff = {
        diff: ''
      }
      current_file_diff[:moved_to] = to unless from == to
      files_diffs[from] = current_file_diff
    else
      current_file_diff[:diff] << "#{current_file_diff[:diff].empty? ? '' : "\n"}#{line}" unless current_file_diff.nil?
    end
  end
  impacted_nodes, impacted_services, impact_global = platform.impacts_from files_diffs
  impacted_services.sort!
  impacted_services.uniq!
  impacted_nodes.sort!
  impacted_nodes.uniq!
  [
    if impact_global
      platform.known_nodes.sort
    else
      (
        impacted_nodes + impacted_services.map do |service|
          service_nodes = select_nodes([{ service: service }])
          smallest_set ? [service_nodes.first].compact : service_nodes
        end
      ).flatten.sort.uniq
    end,
    impacted_nodes,
    impacted_services,
    impact_global
  ]
end

#invalidate_metadata_of(node, property) ⇒ Object

Invalidate a metadata property for a given node

Parameters
  • node (String): Node

  • property (Symbol): The property name



273
274
275
276
277
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 273

def (node, property)
  @metadata_mutex.synchronize do
    @metadata[node].delete(property) if @metadata.key?(node)
  end
end

#known_nodesObject

Get the list of known nodes

Result
  • Array<String>: List of nodes



211
212
213
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 211

def known_nodes
  @nodes_platform.keys
end

#known_nodes_listsObject

Get the list of known nodes lists

Result
  • Array<String>: List of nodes lists’ names



219
220
221
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 219

def known_nodes_lists
  @nodes_list_platform.keys
end

#known_servicesObject

Get the list of known service names

Result
  • Array<String>: List of service names



238
239
240
241
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 238

def known_services
   known_nodes, :services
  known_nodes.map { |node| get_services_of node }.flatten.compact.uniq.sort
end

#metadata_of(node, property) ⇒ Object

Get a metadata property for a given node

Parameters
  • node (String): Node

  • property (Symbol): The property name

Result
  • Object or nil: The node’s metadata value for this property, or nil if none



250
251
252
253
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 250

def (node, property)
  ([node], property) unless @metadata.key?(node) && @metadata[node].key?(property)
  @metadata[node][property]
end

#nodes_from_list(nodes_list, ignore_unknowns: false) ⇒ Object

Get the list of nodes (resolved) belonging to a nodes list

Parameters
  • nodes_list (String): Nodes list name

  • ignore_unknowns (Boolean): Do we ignore unknown nodes? [default = false]

Result
  • Array<String>: List of nodes



230
231
232
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 230

def nodes_from_list(nodes_list, ignore_unknowns: false)
  select_nodes(@nodes_list_platform[nodes_list].nodes_selectors_from_nodes_list(nodes_list), ignore_unknowns: ignore_unknowns)
end

#options_parse(options_parser, parallel: true) ⇒ Object

Complete an option parser with options meant to control this Nodes Handler

Parameters
  • options_parser (OptionParser): The option parser to complete



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 125

def options_parse(options_parser, parallel: true)
  options_parser.separator ''
  options_parser.separator 'Nodes handler options:'
  options_parser.on('-o', '--show-nodes', 'Display the list of possible nodes and exit') do
    out "* Known platforms:\n#{
      @platforms_handler.known_platforms.map do |platform|
        "#{platform.name} - Type: #{platform.platform_type} - Location: #{platform.repository_path}"
      end.sort.join("\n")
    }"
    out
    out "* Known nodes lists:\n#{known_nodes_lists.sort.join("\n")}"
    out
    out "* Known services:\n#{known_services.sort.join("\n")}"
    out
    out "* Known nodes:\n#{known_nodes.sort.join("\n")}"
    out
    out "* Known nodes with description:\n#{
       known_nodes, %i[hostname host_ip private_ips services description]
      known_nodes.map do |node|
        "#{node} (#{
          if get_hostname_of node
            get_hostname_of node
          elsif get_host_ip_of node
            get_host_ip_of node
          elsif get_private_ips_of node
            get_private_ips_of(node).first
          else
            'No connection'
          end
        }) - #{(get_services_of(node) || []).join(', ')} - #{get_description_of(node) || ''}"
      end.sort.join("\n")
    }"
    out
    exit 0
  end
end

#options_parse_nodes_selectors(options_parser, nodes_selectors) ⇒ Object

Complete an option parser with ways to select nodes in parameters

Parameters
  • options_parser (OptionParser): The option parser to complete

  • nodes_selectors (Array): The list of nodes selectors that will be populated by parsing the options



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 167

def options_parse_nodes_selectors(options_parser, nodes_selectors)
  platform_names = @platforms_handler.known_platforms.map(&:name).sort
  options_parser.separator ''
  options_parser.separator 'Nodes selection options:'
  options_parser.on('-a', '--all-nodes', 'Select all nodes') do
    nodes_selectors << { all: true }
  end
  options_parser.on('-b', '--nodes-platform PLATFORM', "Select nodes belonging to a given platform name. Available platforms are: #{platform_names.join(', ')} (can be used several times)") do |platform|
    nodes_selectors << { platform: platform }
  end
  options_parser.on('-l', '--nodes-list LIST', 'Select nodes defined in a nodes list (can be used several times)') do |nodes_list|
    nodes_selectors << { list: nodes_list }
  end
  options_parser.on('-n', '--node NODE', 'Select a specific node. Can be a regular expression to select several nodes if used with enclosing "/" characters. (can be used several times).') do |node|
    nodes_selectors << node
  end
  options_parser.on('-r', '--nodes-service SERVICE', 'Select nodes implementing a given service (can be used several times)') do |service|
    nodes_selectors << { service: service }
  end
  options_parser.on(
    '--nodes-git-impact GIT_IMPACT',
    'Select nodes impacted by a git diff from a platform (can be used several times).',
    'GIT_IMPACT has the format PLATFORM:FROM_COMMIT:TO_COMMIT:FLAGS',
    "* PLATFORM: Name of the platform to check git diff from. Available platforms are: #{platform_names.join(', ')}",
    '* FROM_COMMIT: Commit ID or refspec from which we perform the diff. If ommitted, defaults to master',
    '* TO_COMMIT: Commit ID ot refspec to which we perform the diff. If ommitted, defaults to the currently checked-out files',
    '* FLAGS: Extra comma-separated flags. The following flags are supported:',
    '  - min: If specified then each impacted service will select only 1 node implementing this service. If not specified then all nodes implementing the impacted services will be selected.'
  ) do |nodes_git_impact|
    platform_name, from_commit, to_commit, flags = nodes_git_impact.split(':')
    flags = (flags || '').split(',')
    raise "Invalid platform in --nodes-git-impact: #{platform_name}. Possible values are: #{platform_names.join(', ')}." unless platform_names.include?(platform_name)
    nodes_selector = { platform: platform_name }
    nodes_selector[:from_commit] = from_commit if from_commit && !from_commit.empty?
    nodes_selector[:to_commit] = to_commit if to_commit && !to_commit.empty?
    nodes_selector[:smallest_set] = true if flags.include?('min')
    nodes_selectors << { git_diff: nodes_selector }
  end
end

#override_metadata_of(node, property, value) ⇒ Object

Override a metadata property for a given node

Parameters
  • node (String): Node

  • property (Symbol): The property name

  • value (Object): The property value



261
262
263
264
265
266
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 261

def (node, property, value)
  @metadata_mutex.synchronize do
    @metadata[node] = {} unless @metadata.key?(node)
    @metadata[node][property] = value
  end
end

#prefetch_metadata_of(nodes, properties) ⇒ Object

Prefetch some metadata properties for a given list of nodes. Useful for performance reasons when clients know they will need to use a lot of properties on nodes. Keep a thread-safe memory cache of it.

Parameters
  • nodes (Array<String>): Nodes to read metadata for

  • properties (Symbol or Array<Symbol>): Metadata properties (or single one) to read



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 322

def (nodes, properties)
  (properties.is_a?(Symbol) ? [properties] : properties).each do |property|
    # Gather the list of nodes missing this property
    missing_nodes = nodes.select { |node| !@metadata.key?(node) || !@metadata[node].key?(property) }
    unless missing_nodes.empty?
      # Query the CMDBs having first the get_<property> method, then the ones having the get_others method till we have our property set for all missing nodes
      # Metadata being retrieved by the different CMDBs, per node
      # Hash< String, Object >
       = {}
      (
        (@cmdbs_per_property.key?(property) ? @cmdbs_per_property[property] : []).map { |cmdb| [cmdb, property] } +
          @cmdbs_others.map { |cmdb| [cmdb, :others] }
      ).each do |(cmdb, cmdb_property)|
        # If among the missing nodes some of them have some master CMDB declared for this property, filter them out unless we are dealing with their master CMDB.
        nodes_to_query = missing_nodes.select do |node|
          master_cmdb = cmdb_master_for(node, property)
          master_cmdb.nil? || master_cmdb == cmdb
        end
        unless nodes_to_query.empty?
          # Check first if this property depends on other ones for this cmdb
          if cmdb.respond_to?(:property_dependencies)
            property_deps = cmdb.property_dependencies
             nodes_to_query, property_deps[property] if property_deps.key?(property)
          end
          # Property values, per node name
          # Hash< String, Object >
           = Hash[
            cmdb.send("get_#{cmdb_property}".to_sym, nodes_to_query, @metadata.slice(*nodes_to_query)).map do |node, cmdb_result|
              [node, cmdb_property == :others ? cmdb_result[property] : cmdb_result]
            end
          ].compact
          cmdb_log_header = "[CMDB #{cmdb.class.name.split('::').last}.#{cmdb_property}] -"
          log_debug "#{cmdb_log_header} Query property #{property} for #{nodes_to_query.size} nodes (#{nodes_to_query[0..7].join(', ')}...) => Found metadata for #{.size} nodes."
          .merge!() do |node, existing_value, new_value|
            raise "#{cmdb_log_header} Returned a conflicting value for metadata #{property} of node #{node}: #{new_value} whereas the value was already set to #{existing_value}" if !existing_value.nil? && new_value != existing_value
            new_value
          end
        end
      end
      # Avoid conflicts in metadata while merging and make sure this update is thread-safe
      # As @metadata is only appending data and never deleting it, protecting the update only is enough.
      # At worst several threads will query several times the same CMDBs to update the same data several times.
      # If we also want to be thread-safe in this regard, we should protect the whole CMDB call with mutexes, at the granularity of the node + property bein read.
      @metadata_mutex.synchronize do
        missing_nodes.each do |node|
          @metadata[node] = {} unless @metadata.key?(node)
          # Here, explicitely store nil if nothing has been found for a node because we know there is no value to be fetched.
          # This way we won't query again all CMDBs thanks to the cache.
          @metadata[node][property] = [node]
        end
      end
    end
  end
end

#select_confs_for_node(node, configs) ⇒ Object

Select the configs applicable to a given node.

Parameters
  • node (String): The node for which we select configurations

  • configs (Array< Hash<Symbol,Object> >): Configuration properties. Each configuration is selected based on the nodes_selectors_stack property.

Result
  • Array< Hash<Symbol,Object> >: The selected configurations



537
538
539
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 537

def select_confs_for_node(node, configs)
  configs.select { |config_info| select_from_nodes_selector_stack(config_info[:nodes_selectors_stack]).include?(node) }
end

#select_confs_for_platform(platform_name, configs) ⇒ Object

Select the configs applicable to a given platform.

Parameters
  • platform_name (String): The platform for which we select configurations

  • configs (Array< Hash<Symbol,Object> >): Configuration properties. Each configuration is selected based on the nodes_selectors_stack property.

Result
  • Array< Hash<Symbol,Object> >: The selected configurations



548
549
550
551
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 548

def select_confs_for_platform(platform_name, configs)
  platform_nodes = @platforms_handler.platform(platform_name).known_nodes
  configs.select { |config_info| (platform_nodes - select_from_nodes_selector_stack(config_info[:nodes_selectors_stack])).empty? }
end

#select_from_nodes_selector_stack(nodes_selector_stack) ⇒ Object

Get the list of nodes impacted by a nodes selector stack. The result is the intersection of every nodes set in the stack.

Parameters
  • nodes_selector_stack (Array): The nodes selector stack

Result
  • Array<String>: List of nodes



560
561
562
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 560

def select_from_nodes_selector_stack(nodes_selector_stack)
  nodes_selector_stack.inject(known_nodes) { |selected_nodes, nodes_selector| selected_nodes & select_nodes(nodes_selector) }
end

#select_nodes(*nodes_selectors, ignore_unknowns: false) ⇒ Object

Resolve a list of nodes selectors into a real list of known nodes. A node selector can be:

  • String: Node name, or a node regexp if enclosed within ‘/’ character (ex: ‘/.worker./’)

  • Hash<Symbol,Object>: More complete information that can contain the following keys:

    • all (Boolean): If true, specify that we want all known nodes.

    • list (String): Name of a nodes list.

    • platform (String): Name of a platform containing nodes.

    • service (String): Name of a service implemented by nodes.

    • git_diff (Hash<Symbol,Object>): Info about a git diff that impacts nodes:

      • platform (String): Name of the platform on which checking the git diff

      • from_commit (String): Commit ID to check from [default: ‘master’]

      • to_commit (String or nil): Commit ID to check to, or nil for currently checked-out files [default: nil]

      • smallest_set (Boolean): Smallest set of impacted nodes? [default: false]

Parameters
  • nodes_selectors (Array<Object>): List of node selectors (can be a single element).

  • ignore_unknowns (Boolean): Do we ignore unknown nodes? [default = false]

Result
  • Array<String>: List of nodes



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/hybrid_platforms_conductor/nodes_handler.rb', line 396

def select_nodes(*nodes_selectors, ignore_unknowns: false)
  nodes_selectors = nodes_selectors.flatten
  # 1. Check for the presence of all
  return known_nodes if nodes_selectors.any? { |nodes_selector| nodes_selector.is_a?(Hash) && nodes_selector.key?(:all) && nodes_selector[:all] }
  # 2. Expand the nodes lists, platforms and services contents
  string_nodes = []
  nodes_selectors.each do |nodes_selector|
    if nodes_selector.is_a?(String)
      string_nodes << nodes_selector
    else
      if nodes_selector.key?(:list)
        platform = @nodes_list_platform[nodes_selector[:list]]
        raise "Unknown nodes list: #{nodes_selector[:list]}" if platform.nil?
        string_nodes.concat(platform.nodes_selectors_from_nodes_list(nodes_selector[:list]))
      end
      string_nodes.concat(@platforms_handler.platform(nodes_selector[:platform]).known_nodes) if nodes_selector.key?(:platform)
      if nodes_selector.key?(:service)
         known_nodes, :services
        string_nodes.concat(known_nodes.select { |node| (get_services_of(node) || []).include?(nodes_selector[:service]) })
      end
      if nodes_selector.key?(:git_diff)
        # Default values
        git_diff_info = {
          from_commit: 'master',
          to_commit: nil,
          smallest_set: false
        }.merge(nodes_selector[:git_diff])
        all_impacted_nodes, _impacted_nodes, _impacted_services, _impact_global = impacted_nodes_from_git_diff(
          git_diff_info[:platform],
          from_commit: git_diff_info[:from_commit],
          to_commit: git_diff_info[:to_commit],
          smallest_set: git_diff_info[:smallest_set]
        )
        string_nodes.concat(all_impacted_nodes)
      end
    end
  end
  # 3. Expand the Regexps
  real_nodes = []
  string_nodes.each do |node|
    if node =~ /^\/(.+)\/$/
      node_regexp = Regexp.new($1)
      real_nodes.concat(known_nodes.select { |known_node| known_node[node_regexp] })
    else
      real_nodes << node
    end
  end
  # 4. Sort them unique
  real_nodes.uniq!
  real_nodes.sort!
  # Some sanity checks
  unless ignore_unknowns
    unknown_nodes = real_nodes - known_nodes
    raise "Unknown nodes: #{unknown_nodes.join(', ')}" unless unknown_nodes.empty?
  end
  real_nodes
end