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:



284
285
286
287
288
# File 'lib/mb/node_querier.rb', line 284

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:



299
300
301
302
303
# File 'lib/mb/node_querier.rb', line 299

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:



268
269
270
271
272
# File 'lib/mb/node_querier.rb', line 268

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

  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

Returns:

  • (Ridley::HostConnector::Response)

Raises:



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
145
146
147
148
149
150
# File 'lib/mb/node_querier.rb', line 120

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}") }

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

  if response.error?
    log.info { "Failed Chef client run on: #{host}" }
    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.



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

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.



354
355
356
357
358
359
360
361
362
363
364
365
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
# File 'lib/mb/node_querier.rb', line 354

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) ⇒ Ridley::HostConnection::Response

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

Parameters:

  • host (String)
  • command (String)

Returns:

  • (Ridley::HostConnection::Response)


202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/mb/node_querier.rb', line 202

def execute_command(host, command)

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

  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

Returns:

  • (String, nil)


90
91
92
93
94
95
# File 'lib/mb/node_querier.rb', line 90

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 = {}) ⇒ Object

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.



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/mb/node_querier.rb', line 316

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

  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

Returns:

  • (Ridley::HostConnector::Response)

Raises:



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/mb/node_querier.rb', line 173

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

  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)


231
232
233
# File 'lib/mb/node_querier.rb', line 231

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)


249
250
251
252
253
254
# File 'lib/mb/node_querier.rb', line 249

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