Class: HybridPlatformsConductor::Topographer

Inherits:
Object
  • Object
show all
Includes:
LoggerHelpers
Defined in:
lib/hybrid_platforms_conductor/topographer.rb,
lib/hybrid_platforms_conductor/topographer/plugin.rb,
lib/hybrid_platforms_conductor/topographer/plugins/svg.rb,
lib/hybrid_platforms_conductor/topographer/plugins/json.rb,
lib/hybrid_platforms_conductor/topographer/plugins/graphviz.rb

Overview

Class giving an API to parse the graph of the TI network

Defined Under Namespace

Modules: Plugins Classes: Plugin

Constant Summary

Constants included from LoggerHelpers

LoggerHelpers::LEVELS_MODIFIERS, LoggerHelpers::LEVELS_TO_STDERR

Instance Attribute Summary collapse

Class Method Summary collapse

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

Constructor Details

#initialize(logger: Logger.new($stdout), logger_stderr: Logger.new($stderr), nodes_handler: NodesHandler.new, json_dumper: JsonDumper.new, config: {}) ⇒ Topographer

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)]

  • nodes_handler (NodesHandler): The nodes handler to be used [default = NodesHandler.new]

  • json_dumper (JsonDumper): The JSON Dumper to be used [default = JsonDumper.new]

  • config (Hash<Symbol,Object>): Some configuration parameters that can override defaults. [default = {}] Here are the possible keys:

    • json_files_dir (String): Directory from which JSON files are taken. [default = nodes_json]

    • connections_max_level (Integer or nil): Number maximal of recursive passes to get hostname connections (nil means no limit). [default = nil]



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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 81

def initialize(logger: Logger.new($stdout), logger_stderr: Logger.new($stderr), nodes_handler: NodesHandler.new, json_dumper: JsonDumper.new, config: {})
  init_loggers(logger, logger_stderr)
  @nodes_handler = nodes_handler
  @json_dumper = json_dumper
  @config = Topographer.default_config.merge(config)
  # Get the metadata of each node, per hostname
  # Hash<String,Hash>
   = {}
  # Know for each IP what is the hostname it belongs to
  # Hash<String,String>
  @ips_to_host = {}
  # Get the connection information per node name. A node reprensents 1 element that can be connected to other elements in the graph.
  # Hash< String, Hash<Symbol,Object> >
  # Here are the possible information keys:
  # * *type* (Symbol): Type of the node. Can be one of: :node, :cluster, :unknown.
  # * *connections* (Hash< String, Array<String> >): List of labels per connected node.
  # * *includes* (Array<String>): List of nodes included in this one.
  # * *includes_proc* (Proc): Proc called to know if a node belongs to this cluster [only if type == :cluster]:
  #   * Parameters::
  #     * *node_name* (String): Name of the node for the inclusion test
  #   * Result::
  #     * Boolean: Does the node belongs to this cluster?
  # * *ipv4* (IPAddress::IPv4): Corresponding IPv4 object [only if type == :node and a private IP exists, or type == :unknown, or type == :cluster and the cluster name is an IP range]
  @nodes_graph = {}

  # Default values
  @from_hosts = []
  @to_hosts = []
  @outputs = []
  @skip_run = false

  # Parse plugins
  @plugins = Dir.
    glob("#{__dir__}/topographer/plugins/*.rb").
    to_h do |file_name|
      plugin_name = File.basename(file_name)[0..-4].to_sym
      require file_name
      [
        plugin_name,
        Topographer::Plugins.const_get(plugin_name.to_s.split('_').collect(&:capitalize).join.to_sym)
      ]
    end

  @ips_to_host = known_ips.clone

  # Fill info from the metadata
   = i[
    description
    physical_node
    private_ips
  ]
  @nodes_handler. @nodes_handler.known_nodes, 
  @nodes_handler.known_nodes.each do |hostname|
    [hostname] = .to_h { |property| [property, @nodes_handler.(hostname, property)] }
  end

  # Small cache of hostnames used a lot to parse JSON
  @known_nodes = @nodes_handler.known_nodes.to_h { |hostname| [hostname, nil] }
  # Cache of objects being used a lot in parsing for performance
  @non_word_regexp = /\W+/
  @ip_regexp = %r{(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(/(\d{1,2})|[^\d/]|$)}
  # Cache of ignored IPs
  @ips_ignored = {}
end

Instance Attribute Details

#configObject (readonly)

Some getters that can be useful for clients of the Topographer



69
70
71
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 69

def config
  @config
end

#node_metadataObject (readonly)

Some getters that can be useful for clients of the Topographer



69
70
71
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 69

def 
  
end

#nodes_graphObject (readonly)

Some getters that can be useful for clients of the Topographer



69
70
71
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 69

def nodes_graph
  @nodes_graph
end

Class Method Details

.default_configObject

Give a default configuration

Result
  • Hash<Symbol,Object>: Default configuration



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 19

def self.default_config
  {
    # Directory from which the complete JSON files are to be read
    json_files_dir: 'nodes_json',
    # JSON keys to ignore when reading complete JSON files. Only leafs of this tree structure are ignored.
    ignore_json_keys: {
      # This should only duplicate the real configuration from the recipes, and it adds a lot of IP ranges that can be ignored.
      'network' => nil,
      # Contains simple network definition. Not a connection in itself.
      'policy_xae_outproxy' => { 'local_network' => nil },
      # Contains DNS entries. Not a connection in itself.
      'policy_xae_xx_cdh' => { 'dns' => nil },
      # This contains firewall rules, therefore representing who connects on the host, and not who the host connects to.
      'policy_xae_xx_iptables' => nil,
      # Contains the allowed network range. Not a connection in itself.
      'postfix' => { 'main' => { 'mynetworks' => nil } },
      # This contains sometime IP addresses in the key comments
      'site_directory' => nil,
      # This contains firewall rules, therefore representing who connects on the host, and not who the host connects to.
      'site_iptables' => nil,
      # This contains some user names having IP addresses inside
      'site_xx_roles' => nil,
      # This stores routes for all Proxmox instances.
      'pve' => { 'vlan' => { 'routes' => nil } }
    },
    # JSON keys to ignore when reading complete JSON files, whatever their position
    ignore_any_json_keys: [
      # Those contain cache of MAC addresses to IP addresses
      'arp',
      # Those contain broadcast IP addresses
      'broadcast',
      # Those contain firewall rules, therefore representing who connects on the host, and not who the host connects to.
      'firewall',
      # Those contain version numbers with same format as IP addresses
      'version'
    ],
    # IPs to ignore while parsing complete JSON files
    ignore_ips: [
      /^0\./,
      /^127\./,
      /^255\./
    ],
    # Maximum level of recursion while building the graph of connected nodes (nil = no limit).
    connections_max_level: nil,
    # Maximum label length for a link
    max_link_label_length: 128
  }
end

Instance Method Details

#ancestor_nodes(nodes_list) ⇒ Object

Return the list of nodes and ancestors of a given list of nodes, recursively. An ancestor of a node is another node connected to it, or to a group including it. An ancestor of a node can be:

  • Another node connected to it.

  • Another node including it.

Parameters
  • nodes_list (Array<String>): List of nodes for which we look for ancestors.

Result
  • Array<String>: List of ancestor nodes.



326
327
328
329
330
331
332
333
334
335
336
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 326

def ancestor_nodes(nodes_list)
  ancestor_nodes_list = []
  @nodes_graph.each do |node_name, node_info|
    ancestor_nodes_list << node_name if !nodes_list.include?(node_name) && (!(node_info[:connections].keys & nodes_list).empty? || !(node_info[:includes] & nodes_list).empty?)
  end
  if ancestor_nodes_list.empty?
    nodes_list
  else
    ancestor_nodes(nodes_list + ancestor_nodes_list)
  end
end

#available_pluginsObject

Get the list of available plugins

Result
  • Array<Symbol>: List of plugins



222
223
224
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 222

def available_plugins
  @plugins.keys
end

#children_nodes(nodes_list) ⇒ Object

Return the list of nodes and children of a given list of nodes, recursively. A child of a node is another node connected to it, or to a group including it. A child of a node can be:

  • Another node that it connects to.

  • Another node that it includes.

Parameters
  • nodes_list (Array<String>): List of nodes for which we look for children.

Result
  • Array<String>: List of children nodes.



348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 348

def children_nodes(nodes_list)
  children_nodes_list = []
  nodes_list.each do |node_name|
    children_nodes_list.concat(@nodes_graph[node_name][:connections].keys + @nodes_graph[node_name][:includes])
  end
  children_nodes_list.uniq!
  new_children_nodes = children_nodes_list - nodes_list
  if new_children_nodes.empty?
    children_nodes_list
  else
    children_nodes(children_nodes_list)
  end
end

#cluster_nodesObject

Return the list of nodes that are clusters

Result
  • Array<String>: List of cluster nodes



366
367
368
369
370
371
372
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 366

def cluster_nodes
  cluster_nodes_list = []
  @nodes_graph.each do |node_name, node_info|
    cluster_nodes_list << node_name if node_info[:type] == :cluster
  end
  cluster_nodes_list
end

#collapse_nodes(nodes_list) ⇒ Object

Collapse a given list of nodes.

Parameters
  • nodes_list (Array<String>): List of nodes to collapse



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 271

def collapse_nodes(nodes_list)
  nodes_list.each do |node_name_to_collapse|
    included_nodes = @nodes_graph[node_name_to_collapse][:includes]
    # First collapse its included nodes if any
    collapse_nodes(included_nodes)
    # Then collapse this one
    collapsed_connections = {}
    included_nodes.each do |included_node_name|
      collapsed_connections.merge!(@nodes_graph[included_node_name][:connections]) { |_connected_node, labels_1, labels_2| (labels_1 + labels_2).uniq }
    end
    @nodes_graph[node_name_to_collapse][:connections] = collapsed_connections
    @nodes_graph[node_name_to_collapse][:includes] = []
    replace_nodes(included_nodes, node_name_to_collapse)
  end
end

#define_clusters_ip_24Object

Define clusters of ips with 24 bits ranges.



305
306
307
308
309
310
311
312
313
314
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 305

def define_clusters_ip_24
  # Clone keys as we modify the hash in the loop
  @nodes_graph.keys.clone.each do |node_name|
    next unless @nodes_graph[node_name][:type] == :node && ![node_name][:private_ips].nil? && ![node_name][:private_ips].empty?

    ip_24 = "#{@node_metadata[node_name][:private_ips].first.split('.')[0..2].join('.')}.0/24"
    @nodes_graph[ip_24] = ip_range_graph_info(ip_24) unless @nodes_graph.key?(ip_24)
    @nodes_graph[ip_24][:includes] << node_name unless @nodes_graph[ip_24][:includes].include?(node_name)
  end
end

#description_for(node_name) ⇒ Object

Get the description of a given node

Parameters
  • node_name (String): Node name

Result
  • String: Node description, or nil if none



550
551
552
553
554
555
556
557
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 550

def description_for(node_name)
  case @nodes_graph[node_name][:type]
  when :node
    [node_name][:description]
  when :cluster, :unknown
    nil
  end
end

#dump_outputsObject

Dump the graph in the desired outputs



210
211
212
213
214
215
216
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 210

def dump_outputs
  @outputs.each do |(format, file_name)|
    section "Write #{format} file #{file_name}" do
      write_graph(file_name, format)
    end
  end
end

#filter_in_nodes(nodes_list) ⇒ Object

Remove from the graph any node that is not part of a given list

Parameters
  • nodes_list (Array<String>): List of nodes to keep



378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 378

def filter_in_nodes(nodes_list)
  new_nodes_graph = {}
  @nodes_graph.each do |node_name, node_info|
    next unless nodes_list.include?(node_name)

    new_nodes_graph[node_name] = node_info.merge(
      connections: node_info[:connections].select { |connected_hostname, _labels| nodes_list.include?(connected_hostname) },
      includes: node_info[:includes] & nodes_list
    )
  end
  @nodes_graph = new_nodes_graph
end

#filter_out_nodes(nodes_list) ⇒ Object

Remove from the graph any node that is part of a given list

Parameters
  • nodes_list (Array<String>): List of nodes to remove



395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 395

def filter_out_nodes(nodes_list)
  new_nodes_graph = {}
  @nodes_graph.each do |node_name, node_info|
    next if nodes_list.include?(node_name)

    new_nodes_graph[node_name] = node_info.merge(
      connections: node_info[:connections].reject { |connected_hostname, _labels| nodes_list.include?(connected_hostname) },
      includes: node_info[:includes] - nodes_list
    )
  end
  @nodes_graph = new_nodes_graph
end

#force_cluster_strict_hierarchyObject

Make sure clusters follow a strict hierarchy and that 1 node belongs to at most 1 cluster.



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 434

def force_cluster_strict_hierarchy
  # Find the nodes belonging to several clusters.
  loop do
    # First cluster found each node name
    # Hash<String, String >
    cluster_per_node = {}
    conflicting_clusters = nil
    @nodes_graph.each do |node_name, node_info|
      node_info[:includes].each do |included_node_name|
        if cluster_per_node.key?(included_node_name)
          # Found a conflict between 2 clusters
          conflicting_clusters = [node_name, cluster_per_node[included_node_name]]
          log_error "Node #{included_node_name} found in both clusters #{node_name} and #{cluster_per_node[included_node_name]}"
          break
        else
          cluster_per_node[included_node_name] = node_name
        end
      end
      break unless conflicting_clusters.nil?
    end
    break if conflicting_clusters.nil?

    # We have conflicting clusters to resolve
    cluster_1, cluster_2 = conflicting_clusters
    cluster_1_belongs_to_cluster_2 = @nodes_graph[cluster_1][:includes].all? { |cluster_1_node_name| @nodes_graph[cluster_2][:includes_proc].call(cluster_1_node_name) }
    cluster_2_belongs_to_cluster_1 = @nodes_graph[cluster_2][:includes].all? { |cluster_2_node_name| @nodes_graph[cluster_1][:includes_proc].call(cluster_2_node_name) }
    if cluster_1_belongs_to_cluster_2
      if cluster_2_belongs_to_cluster_1
        # Both clusters have the same nodes
        if @nodes_graph[cluster_1][:includes_proc].call(cluster_2)
          @nodes_graph[cluster_2][:includes] = (@nodes_graph[cluster_1][:includes] + @nodes_graph[cluster_2][:includes]).uniq
          @nodes_graph[cluster_1][:includes] = [cluster_2]
        else
          @nodes_graph[cluster_1][:includes] = (@nodes_graph[cluster_1][:includes] + @nodes_graph[cluster_2][:includes]).uniq
          @nodes_graph[cluster_2][:includes] = [cluster_1]
        end
      else
        # All nodes of cluster_1 belong to cluster_2, but some nodes of cluster_2 don't belong to cluster_1
        @nodes_graph[cluster_2][:includes] = @nodes_graph[cluster_2][:includes] - @nodes_graph[cluster_1][:includes] + [cluster_1]
      end
    elsif cluster_2_belongs_to_cluster_1
      # All nodes of cluster_2 belong to cluster_1, but some nodes of cluster_1 don't belong to cluster_2
      @nodes_graph[cluster_1][:includes] = @nodes_graph[cluster_1][:includes] - @nodes_graph[cluster_2][:includes] + [cluster_2]
    else
      # cluster_1 and cluster_2 have to be merged
      new_cluster_name = "#{cluster_1}_&_#{cluster_2}"
      # Store thos proc in those variables as the cluster_1 and cluster_2 references are going to be removed
      includes_proc_1 = @nodes_graph[cluster_1][:includes_proc]
      includes_proc_2 = @nodes_graph[cluster_2][:includes_proc]
      @nodes_graph[new_cluster_name] = {
        type: :cluster,
        includes: (@nodes_graph[cluster_1][:includes] + @nodes_graph[cluster_2][:includes]).uniq,
        connections: @nodes_graph[cluster_1][:connections].merge!(@nodes_graph[cluster_2][:connections]) { |_connected_node, labels_1, labels_2| (labels_1 + labels_2).uniq },
        includes_proc: proc do |hostname|
          includes_proc_1.call(hostname) || includes_proc_2.call(hostname)
        end
      }
      replace_nodes([cluster_1, cluster_2], new_cluster_name)
    end
  end
end

#graph_for(hostnames) ⇒ Object

Add to the graph a given set of hostnames and their connected nodes.

Parameters
  • hostnames (Array<String>): List of hostnames



230
231
232
233
234
235
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 230

def graph_for(hostnames)
  # Parse connections from JSON files
  hostnames.each do |hostname|
    parse_connections_for(hostname, @config[:connections_max_level])
  end
end

#graph_for_nodes_lists(nodes_lists, only_add_cluster: false) ⇒ Object

Add to the graph a given set of nodes lists and their connected nodes.

Parameters
  • nodes_lists (Array<String>): List of nodes lists

  • only_add_cluster (Boolean): If true, then don’t add missing nodes from this graph to the graph [default = false]



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 242

def graph_for_nodes_lists(nodes_lists, only_add_cluster: false)
  nodes_lists.each do |nodes_list|
    hosts_list = @nodes_handler.select_nodes(@nodes_handler.nodes_from_list(nodes_list))
    if only_add_cluster
      # Select only the hosts list we know about
      hosts_list.select! { |hostname| @nodes_graph.key?(hostname) }
    else
      # Parse JSON for all the hosts of this cluster
      hosts_list.each do |hostname|
        parse_connections_for(hostname, @config[:connections_max_level])
      end
    end
    unless @nodes_graph.key?(nodes_list)
      @nodes_graph[nodes_list] = {
        type: :cluster,
        connections: {},
        includes: [],
        includes_proc: proc { |node_name| hosts_list.include?(node_name) }
      }
    end
    @nodes_graph[nodes_list][:includes].concat(hosts_list)
    @nodes_graph[nodes_list][:includes].uniq!
  end
end

#json_filesObject

Generate the JSON files to be used



201
202
203
204
205
206
207
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 201

def json_files
  return if @skip_run

  @json_dumper.dump_dir = @config[:json_files_dir]
  # Generate all the jsons, even if 1 hostname is given, as it might be useful for the rest of the graph.
  @json_dumper.dump_json_for(@nodes_handler.known_nodes)
end

#node_cluster?(node_name) ⇒ Boolean

Is the node represented as a cluster?

Parameters
  • node_name (String): Node name

Result
  • Boolean: Is the node represented as a cluster?

Returns:

  • (Boolean)


502
503
504
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 502

def node_cluster?(node_name)
  @nodes_graph[node_name][:type] == :cluster || !@nodes_graph[node_name][:includes].empty?
end

#node_physical?(node_name) ⇒ Boolean

Is the node a physical node?

Parameters
  • node_name (String): Node name

Result
  • Boolean: Is the node a physical node?

Returns:

  • (Boolean)


512
513
514
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 512

def node_physical?(node_name)
  @nodes_graph[node_name][:type] == :node && [node_name][:physical_node]
end

#options_parse(options_parser) ⇒ Object

Complete an option parser with ways to tune the topographer

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



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 150

def options_parse(options_parser)
  from_hosts_opts_parser = OptionParser.new do |opts|
    @nodes_handler.options_parse_nodes_selectors(opts, @from_hosts)
  end
  to_hosts_opts_parser = OptionParser.new do |opts|
    @nodes_handler.options_parse_nodes_selectors(opts, @to_hosts)
  end
  options_parser.separator ''
  options_parser.separator 'Topographer options:'
  options_parser.on('-F', '--from HOSTS_OPTIONS', 'Specify options for the set of nodes to start from (enclose them with ""). Default: all nodes. HOSTS_OPTIONS follows the following:', *from_hosts_opts_parser.to_s.split("\n")[3..]) do |hosts_options|
    args = hosts_options.split
    from_hosts_opts_parser.parse!(args)
    raise "Unknown --from options: #{args.join(' ')}" unless args.empty?
  end
  options_parser.on('-k', '--skip-run', "Skip the actual gathering of JSON node files. If set, the current files in #{@config[:json_files_dir]} will be used.") do
    @skip_run = true
  end
  options_parser.on('-p', '--output FORMAT:FILE_NAME', "Specify a format and file name. Can be used several times. FORMAT can be one of #{available_plugins.sort.join(', ')}. Ex.: graphviz:graph.gv") do |output|
    format_str, file_name = output.split(':')
    format = format_str.to_sym
    raise "Unknown format: #{format}." unless available_plugins.include?(format)

    @outputs << [format, file_name]
  end
  options_parser.on('-T', '--to HOSTS_OPTIONS', 'Specify options for the set of nodes to get to (enclose them with ""). Default: all nodes. HOSTS_OPTIONS follows the following:', *to_hosts_opts_parser.to_s.split("\n")[3..]) do |hosts_options|
    args = hosts_options.split
    to_hosts_opts_parser.parse!(args)
    raise "Unknown --to options: #{args.join(' ')}" unless args.empty?
  end
end

#remove_empty_clustersObject

Remove empty clusters



295
296
297
298
299
300
301
302
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 295

def remove_empty_clusters
  loop do
    empty_clusters = @nodes_graph.keys.select { |node_name| @nodes_graph[node_name][:type] == :cluster && @nodes_graph[node_name][:includes].empty? }
    break if empty_clusters.empty?

    filter_out_nodes(empty_clusters)
  end
end

#remove_self_connectionsObject

Remove self connections.



288
289
290
291
292
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 288

def remove_self_connections
  @nodes_graph.each do |node_name, node_info|
    node_info[:connections].delete_if { |connected_node_name, _labels| connected_node_name == node_name }
  end
end

#replace_nodes(nodes_to_be_replaced, replacement_node) ⇒ Object

Replace a list of nodes by a given node.

Parameters
  • nodes_to_be_replaced (Array<String>): Nodes to be replaced

  • replacement_node (String): Node that is used for replacement



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 413

def replace_nodes(nodes_to_be_replaced, replacement_node)
  # Delete references to the nodes to be replaced
  @nodes_graph.delete_if { |node_name, _node_info| nodes_to_be_replaced.include?(node_name) }
  # Change any connection or inclusions using nodes to be replaced
  @nodes_graph.each_value do |node_info|
    node_info[:includes] = node_info[:includes].map { |included_node_name| nodes_to_be_replaced.include?(included_node_name) ? replacement_node : included_node_name }.uniq
    new_connections = {}
    node_info[:connections].each do |connected_node_name, labels|
      if nodes_to_be_replaced.include?(connected_node_name)
        new_connections[replacement_node] = [] unless new_connections.key?(replacement_node)
        new_connections[replacement_node].concat(labels)
        new_connections[replacement_node].uniq!
      else
        new_connections[connected_node_name] = labels
      end
    end
    node_info[:connections] = new_connections
  end
end

#resolve_from_toObject

Resolve the from and to hosts descriptions

Result
  • Array<String>: The from hostnames

  • Array<String>: The to hostnames



191
192
193
194
195
196
197
198
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 191

def resolve_from_to
  @from_hosts << { all: true } if @from_hosts.empty?
  @to_hosts << { all: true } if @to_hosts.empty?
  [
    @nodes_handler.select_nodes(@from_hosts),
    @nodes_handler.select_nodes(@to_hosts)
  ]
end

#title_for(node_name) ⇒ Object

Get the title of a given node

Parameters
  • node_name (String): Node name

Result
  • String: Node title



533
534
535
536
537
538
539
540
541
542
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 533

def title_for(node_name)
  case @nodes_graph[node_name][:type]
  when :node
    "#{node_name} - #{@node_metadata[node_name][:private_ips].nil? || @node_metadata[node_name][:private_ips].empty? ? 'No IP' : @node_metadata[node_name][:private_ips].first}"
  when :cluster
    "#{node_name} (#{@nodes_graph[node_name][:includes].size} nodes)"
  when :unknown
    "#{node_name} - Unknown node"
  end
end

#validate_paramsObject

Validate that parsed parameters are valid



182
183
184
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 182

def validate_params
  raise 'No output defined. Please use --output option.' if @outputs.empty?
end

#write_graph(file_name, output_format) ⇒ Object

Output the graph to a given file at a given format

Parameters
  • file_name (String): File name to output to.

  • output_format (Symbol): Output format to use (should be part of the plugins).



521
522
523
524
525
# File 'lib/hybrid_platforms_conductor/topographer.rb', line 521

def write_graph(file_name, output_format)
  raise "Unknown topographer plugin #{output_format}" unless @plugins.key?(output_format)

  @plugins[output_format].new(self).write_graph(file_name)
end