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, node_object: node, 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
151
152
153
154
155
156
157
158
159
# 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]
      node = options[:node_object]
      node.reload
      old_recipes = node.automatic_attributes.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 = chef_connection.node.execute_command(host, "chef-client --override-runlist #{cmd_recipe_syntax}")

      # reset the run list
      node.reload
      log.info { "Resetting node's recipes attribute back to #{old_recipes}" }
      node.automatic_attributes.recipes = old_recipes
      node.save

      chef_run_response
    else
      log.info { "Running Chef client on: #{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.



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

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 = chef_connection.node.find(node_name)
  required_run_list = []
  chef_synchronize(chef_environment: node.chef_environment, force: options[:force], job: job) do
    if node.run_list.include?(DISABLED_RUN_LIST_ENTRY)
      job.report_success("#{node.name} is already disabled.")
    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 !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.report_success "#{node.name} disabled."
    else
      job.report_failure "#{node.name} did not save! Disabled run_list entry was unable to be added to the node."
    end
  end
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.



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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/mb/node_querier.rb', line 349

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 = chef_connection.node.find(node_name)

  required_run_list = []
  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.report_success "#{node.name} enabled successfully."
      else
        job.report_failure "#{node.name} did not save! Disabled run_list entry was unable to be removed to the node."
      end
    else
      job.report_success("#{node.name} is not disabled. No need to enable.")
    end
  end
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)


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

def execute_command(host, command)
  response = 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
# 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))
  futures.map(&:value)

  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:



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

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

  response = 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
  chef_connection.client.find(client_id).try(:name)
end