Class: MotherBrain::NodeQuerier

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Celluloid, MB::Mixin::Locks, MB::Mixin::Services, Logging
Defined in:
lib/mb/node_querier.rb

Constant Summary collapse

DISABLED_RUN_LIST_ENTRY =
"recipe[disabled]".freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

add_argument_header, dev, filename, #log_exception, logger, #logger, reset, set_logger, setup

Constructor Details

#initializeNodeQuerier

Returns a new instance of NodeQuerier.



25
26
27
# File 'lib/mb/node_querier.rb', line 25

def initialize
  log.debug { "Node Querier starting..." }
end

Class Method Details

.instanceCelluloid::Actor(NodeQuerier)

Returns:

Raises:

  • (Celluloid::DeadActorError)

    if Node Querier has not been started



12
13
14
# File 'lib/mb/node_querier.rb', line 12

def instance
  MB::Application[:node_querier] or raise Celluloid::DeadActorError, "node querier not running"
end

Instance Method Details

#async_disable(host, options = {}) ⇒ MB::JobTicket

Asynchronously disable a node to stop services @host and prevent chef-client from being run on @host until @host is reenabled

Parameters:

  • host (String)

    public hostname of the target node

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :force (Boolean) — default: false

    Ignore environment lock and execute anyway.

Returns:



292
293
294
295
296
# File 'lib/mb/node_querier.rb', line 292

def async_disable(host, options = {})
  job = Job.new(:disable_node)
  async(:disable, job, host, options)
  job.ticket
end

#async_enable(host, options = {}) ⇒ MB::JobTicket

Asynchronously enable a node

Parameters:

  • host (String)

    public hostname of the target node

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :force (Boolean) — default: false

    Ignore environment lock and execute anyway.

Returns:



307
308
309
310
311
# File 'lib/mb/node_querier.rb', line 307

def async_enable(host, options = {})
  job = Job.new(:enable_node)
  async(:enable, job, host, options)
  job.ticket
end

#async_purge(host, options = {}) ⇒ MB::JobTicket

Asynchronously remove Chef from a target host and purge it’s client and node object from the Chef server.

Parameters:

  • host (String)

    public hostname of the target node

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :skip_chef (Boolean) — default: false

    skip removal of the Chef package and the contents of the installation directory. Setting this to true will only remove any data and configurations generated by running Chef client.

Returns:



276
277
278
279
280
# File 'lib/mb/node_querier.rb', line 276

def async_purge(host, options = {})
  job = Job.new(:purge_node)
  async(:purge, job, host, options)
  job.ticket
end

#bulk_chef_run(job, nodes, override_recipes = nil) ⇒ Object

Run Chef on a group of nodes, and update a job status with the result

Parameters:

  • job (Job)
  • nodes (Array(Ridley::NodeResource))

    The collection of nodes to run Chef on

  • override_recipes (Array<String>) (defaults to: nil)

    An array of run list entries that will override the node’s current run list

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/mb/node_querier.rb', line 44

def bulk_chef_run(job, nodes, override_recipes = nil)
  job.set_status("Performing a chef client run on #{nodes.collect(&:name).join(', ')}")

  node_successes_count = 0
  node_successes = Array.new

  node_failures_count  = 0
  node_failures = Array.new

  futures = nodes.map { |node| node_querier.future(:chef_run, node.public_hostname, override_recipes: override_recipes, connector: connector_for_os(node.chef_attributes.os)) }

  futures.each do |future|
    begin
      response = future.value
      node_successes_count += 1
      node_successes << response.host
    rescue RemoteCommandError => error
      node_failures_count += 1
      node_failures << error.host
    end
  end

  if node_failures_count > 0
    abort RemoteCommandError.new("chef client run failed on #{node_failures_count} node(s) - #{node_failures.join(', ')}")
  else
    job.set_status("Finished chef client run on #{node_successes_count} node(s) - #{node_successes.join(', ')}")
  end
end

#chef_run(host, options = {}) ⇒ Ridley::HostConnector::Response

Run Chef-Client on the target host

Parameters:

  • host (String)
  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :user (String)

    a shell user that will login to each node and perform the bootstrap command on (required)

  • :password (String)

    the password for the shell user that will perform the bootstrap

  • :keys (Array, String)

    an array of keys (or a single key) to authenticate the ssh user with instead of a password

  • :timeout (Float) — default: 10.0

    timeout value for SSH bootstrap

  • :sudo (Boolean)

    bootstrap with sudo

  • :override_recipe (String)

    a recipe that will override the nodes current run list

  • :node (Ridley::NodeObject)

    the actual node object

  • :connector (String)

    a connector type for the chef connection to prefer

Returns:

  • (Ridley::HostConnector::Response)

Raises:



124
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
# File 'lib/mb/node_querier.rb', line 124

def chef_run(host, options = {})
  options = options.dup

  unless host.present?
    abort RemoteCommandError.new("cannot execute a chef-run without a hostname or ipaddress")
  end

  response = if options[:override_recipes]
    override_recipes = options[:override_recipes]

    cmd_recipe_syntax = override_recipes.join(',') { |recipe| "recipe[#{recipe}]" }
    log.info { "Running Chef client with override runlist '#{cmd_recipe_syntax}' on: #{host}" }
    chef_run_response = safe_remote(host) { chef_connection.node.execute_command(host, "chef-client --override-runlist #{cmd_recipe_syntax}", connector: options[:connector]) }

    chef_run_response
  else
    log.info { "Running Chef client on: #{host}" }
    safe_remote(host) { chef_connection.node.chef_run(host, connector: options[:connector]) }
  end

  if response.error?
    log.info { "Failed Chef client run on: #{host} - #{response.stderr.chomp}" }
    abort RemoteCommandError.new(response.stderr.chomp, host)
  end

  log.info { "Completed Chef client run on: #{host}" }
  response
rescue Ridley::Errors::HostConnectionError => ex
  log.info { "Failed Chef client run on: #{host}" }
  abort RemoteCommandError.new(ex, host)
end

#disable(job, host, options = {}) ⇒ Object

Stop services on @host and prevent chef-client from being run on

Parameters:

  • job (MB::Job)
  • host (String)

    public hostname of the target node

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :force (Boolean) — default: false

    Ignore environment lock and execute anyway.



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
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
# File 'lib/mb/node_querier.rb', line 427

def disable(job, host, options = {})
  job.report_running("Discovering host's registered node name")
  node_name = registered_as(host)
  if !node_name
    # TODO auth could fail and cause this to throw
    job.report_failure("Could not discover the host's node name. The host may not be " +
                       "registered with Chef or the embedded Ruby used to identify the " +
                       "node name may not be available. #{host} was not disabled!")
  end
  job.set_status("Host registered as #{node_name}.")

  node = fetch_node(job, node_name)

  required_run_list = []
  success = false
  chef_synchronize(chef_environment: node.chef_environment, force: options[:force], job: job) do
    if node.run_list.include?(DISABLED_RUN_LIST_ENTRY)
      job.set_status("#{node.name} is already disabled.")
      success = true
    else
      required_run_list = on_dynamic_services(job, node) do |dynamic_service, plugin|
        dynamic_service.node_state_change(job,
                                          plugin,
                                          node,
                                          MB::Gear::DynamicService::STOP,
                                          false)
      end
    end

    if !success
      if !required_run_list.empty?
        job.set_status "Running chef with the following run list: #{required_run_list.inspect}"
        self.bulk_chef_run(job, [node], required_run_list)
      else
        job.set_status "No recipes required to run."
      end

      node.run_list = [DISABLED_RUN_LIST_ENTRY].concat(node.run_list)
      if node.save
        job.set_status "#{node.name} disabled."
        success = true
      else
        job.set_status "#{node.name} did not save! Disabled run_list entry was unable to be added to the node."
      end
    end
  end
  job.report_boolean(success)
rescue MotherBrain::ResourceLocked => e
  job.report_failure e.message
ensure
  job.terminate if job && job.alive?
end

#enable(job, host, options = {}) ⇒ Object

Remove explicit service state on @host and remove disabled entry from run list to allow chef-client to run on @host

Parameters:

  • job (MB::Job)
  • host (String)

    public hostname of the target node

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :force (Boolean) — default: false

    Ignore environment lock and execute anyway.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/mb/node_querier.rb', line 366

def enable(job, host, options = {})
  job.report_running("Discovering host's registered node name")
  node_name = registered_as(host)
  
  if !node_name
    # TODO auth could fail and cause this to throw
    job.report_failure("Could not discover the host's node name. The host may not be " +
                       "registered with Chef or the embedded Ruby used to identify the " +
                       "node name may not be available. #{host} was not enabled!")
  end
  
  job.set_status("Host registered as #{node_name}.")

  node = fetch_node(job, node_name)

  required_run_list = []
  success = false
  chef_synchronize(chef_environment: node.chef_environment, force: options[:force], job: job) do
    if node.run_list.include?(DISABLED_RUN_LIST_ENTRY)
      required_run_list = on_dynamic_services(job, node) do |dynamic_service, plugin|
        dynamic_service.remove_node_state_change(job,
                                                 plugin,
                                                 node,
                                                 false)

      end
      if !required_run_list.empty?
        self.bulk_chef_run(job, [node], required_run_list.flatten.uniq) 
      end

      node.run_list = node.run_list.reject { |r| r == DISABLED_RUN_LIST_ENTRY }
      
      if node.save
        job.set_status "#{node.name} enabled successfully."
        success = true
      else
        job.set_status "#{node.name} did not save! Disabled run_list entry was unable to be removed to the node."
      end
    else
      job.set_status("#{node.name} is not disabled. No need to enable.")
      success = true
    end
  end

  job.report_boolean(success)
rescue MotherBrain::ResourceLocked => e
  job.report_failure e.message
ensure
  job.terminate if job && job.alive?
end

#execute_command(host, command, options = {}) ⇒ Ridley::HostConnection::Response

Executes the given command on the host using the best worker available for the host.

Parameters:

  • host (String)
  • command (String)
  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :connector (String)

    a connector type for the chef connection to prefer

Returns:

  • (Ridley::HostConnection::Response)


210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/mb/node_querier.rb', line 210

def execute_command(host, command, options = {})

  unless host.present?
    abort RemoteCommandError.new("cannot execute command without a hostname or ipaddress")
  end

  response = safe_remote(host) { chef_connection.node.execute_command(host, command, connector: options[:connector]) }

  if response.error?
    log.info { "Failed to execute command on: #{host}" }
    abort RemoteCommandError.new(response.stderr.chomp)
  end

  log.info { "Successfully executed command on: #{host}" }
  response
end

#listArray<Hash>

List all of the nodes on the target Chef Server

Returns:

  • (Array<Hash>)


32
33
34
# File 'lib/mb/node_querier.rb', line 32

def list
  chef_connection.node.all
end

#node_name(host, options = {}) ⇒ String?

Return the Chef node_name of the target host. A nil value is returned if a node_name cannot be determined

Parameters:

  • host (String)

    hostname of the target node

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :user (String)

    a shell user that will login to each node and perform the bootstrap command on (required)

  • :password (String)

    the password for the shell user that will perform the bootstrap

  • :keys (Array, String)

    an array of keys (or a single key) to authenticate the ssh user with instead of a password

  • :timeout (Float) — default: 10.0

    timeout value for SSH bootstrap

  • :sudo (Boolean) — default: true

    bootstrap with sudo

  • :connector (String)

    a connector type for the chef connection to prefer

Returns:

  • (String, nil)


92
93
94
95
96
97
# File 'lib/mb/node_querier.rb', line 92

def node_name(host, options = {})
  ruby_script('node_name', host, options).split("\n").last
rescue MB::RemoteScriptError
  # TODO: catch auth error?
  nil
end

#purge(job, host, options = {}) ⇒ MB::JobTicket

Remove Chef from a target host and purge it’s client and node object from the Chef server.

Parameters:

  • job (MB::Job)
  • host (String)

    public hostname of the target node

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :skip_chef (Boolean) — default: false

    skip removal of the Chef package and the contents of the installation directory. Setting this to true will only remove any data and configurations generated by running Chef client.

  • :connector (String)

    a connector type for the chef connection to prefer

Returns:



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
# File 'lib/mb/node_querier.rb', line 328

def purge(job, host, options = {})
  options = options.reverse_merge(skip_chef: false)
  futures = Array.new

  job.report_running("Discovering host's registered node name")
  if node_name = registered_as(host)
    job.set_status("Host registered as #{node_name}. Destroying client and node objects.")
    futures << chef_connection.client.future(:delete, node_name)
    futures << chef_connection.node.future(:delete, node_name)
  else
    job.set_status "Could not discover the host's node name. The host may not be registered with Chef or the " +
      "embedded Ruby used to identify the node name may not be available."
  end

  job.set_status("Cleaning up the host's file system.")
  futures << chef_connection.node.future(:uninstall_chef, host, options.slice(:skip_chef, :connector))

  begin
    safe_remote(host) { futures.map(&:value) }
  rescue RemoteCommandError => e
    job.report_failure
  end

  job.report_success
ensure
  job.terminate if job && job.alive?
end

#put_secret(host, options = {}) ⇒ Ridley::HostConnector::Response

Place an encrypted data bag secret on the target host

Parameters:

  • host (String)
  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :secret (String)

    the encrypted data bag secret of the node querier’s chef conn will be used as the default key

  • :user (String)

    a shell user that will login to each node and perform the bootstrap command on (required)

  • :password (String)

    the password for the shell user that will perform the bootstrap

  • :keys (Array, String)

    an array of keys (or a single key) to authenticate the ssh user with instead of a password

  • :timeout (Float) — default: 10.0

    timeout value for SSH bootstrap

  • :sudo (Boolean)

    bootstrap with sudo

  • :connector (String)

    a connector type for the chef connection to prefer

Returns:

  • (Ridley::HostConnector::Response)

Raises:



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/mb/node_querier.rb', line 179

def put_secret(host, options = {})
  options = options.reverse_merge(secret: Application.config.chef.encrypted_data_bag_secret_path)

  if options[:secret].nil? || !File.exists?(options[:secret])
    return nil
  end

  unless host.present?
    abort RemoteCommandError.new("cannot put_secret without a hostname or ipaddress")
  end

  response = safe_remote(host) { chef_connection.node.put_secret(host, connector: options[:connector]) }

  if response.error?
    log.info { "Failed to put secret file on: #{host}" }
    return nil
  end

  log.info { "Successfully put secret file on: #{host}" }
  response
end

#registered?(host) ⇒ Boolean

Check if the target host is registered with the Chef server. If the node does not have Chef and ruby installed by omnibus it will be considered unregistered.

Examples:

showing a node who is registered to Chef

node_querier.registered?("192.168.1.101") #=> true

showing a node who does not have ruby or is not registered to Chef

node_querier.registered?("192.168.1.102") #=> false

Parameters:

  • host (String)

    public hostname of the target node

Returns:

  • (Boolean)


239
240
241
# File 'lib/mb/node_querier.rb', line 239

def registered?(host)
  !!registered_as(host)
end

#registered_as(host) ⇒ String?

Returns the client name the target node is registered to Chef with.

If the node does not have a client registered with the Chef server or if Chef and ruby were not installed by omnibus this function will return nil.

Examples:

showing a node who is registered to Chef

node_querier.registered_as("192.168.1.101") #=> "reset.riotgames.com"

showing a node who does not have ruby or is not registered to Chef

node_querier.registered_as("192.168.1.102") #=> nil

Parameters:

  • host (String)

    public hostname of the target node

Returns:

  • (String, nil)


257
258
259
260
261
262
# File 'lib/mb/node_querier.rb', line 257

def registered_as(host)
  if (client_id = node_name(host)).nil?
    return nil
  end
  safe_remote(host) { chef_connection.client.find(client_id).try(:name) }
end