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:



312
313
314
315
316
# File 'lib/mb/node_querier.rb', line 312

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:



327
328
329
330
331
# File 'lib/mb/node_querier.rb', line 327

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

#async_upgrade_omnibus(version, nodes, options = {}) ⇒ Mb::JobTicket

Asynchronously upgrade the Omnibus installation of Chef on the given nodes to a specific version.

Parameters:

  • version (String)

    the version of Chef to upgrade to

  • nodes (Array<Ridley::NodeObject>)

    the node(s) to upgrade omnibus on

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

    a customizable set of options

Options Hash (options):

  • :prerelease (Boolean)

    whether or not to use a prerelease version of Chef

  • :direct_url (String)

    a URL pointing directly to a Chef package to install

Returns:

  • (Mb::JobTicket)


296
297
298
299
300
# File 'lib/mb/node_querier.rb', line 296

def async_upgrade_omnibus(version, nodes, options = {})
  job = Job.new(:upgrade_omnibus)
  async(:upgrade_omnibus, job, version, nodes, 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

#connector_for_os(os) ⇒ String

Returns a String representing the best connector type to use when communicating with a given node

Parameters:

  • os (String)

    the operating system

Returns:

  • (String)


544
545
546
547
548
549
550
551
552
553
# File 'lib/mb/node_querier.rb', line 544

def connector_for_os(os)
  case os
  when "windows"
    Ridley::HostCommander::DEFAULT_WINDOWS_CONNECTOR
  when "linux"
    Ridley::HostCommander::DEFAULT_LINUX_CONNECTOR
  else
    nil
  end
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.



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
529
530
531
532
533
534
535
# File 'lib/mb/node_querier.rb', line 484

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.



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
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/mb/node_querier.rb', line 423

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:



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

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

#upgrade_omnibus(job, version, nodes, options = {}) ⇒ MB::Job

Upgrades the Omnibus installation of Chef on a specific node(s) to a specific version.

Parameters:

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

    the version of Chef to upgrade to

  • nodes (Array<Ridley::NodeObject>)

    the node(s) to upgrade omnibus on

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

    a customizable set of options

Options Hash (options):

  • :prerelease (Boolean)

    whether or not to use a prerelease version of Chef

  • :direct_url (String)

    a URL pointing directly to a Chef package to install

Returns:



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/mb/node_querier.rb', line 391

def upgrade_omnibus(job, version, nodes, options = {})
  futures = Array.new
  options = options.merge(chef_version: version)

  hostnames = nodes.collect(&:public_hostname)
  job.report_running("Upgrading Omnibus Chef installation on #{hostnames}")

  hostnames.each do |hostname|
    futures << chef_connection.node.future(:update_omnibus, hostname, options)
  end

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

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