Class: MotherBrain::PluginManager

Inherits:
Object
  • Object
show all
Includes:
Celluloid, Celluloid::Notifications, MB::Mixin::Services, Logging
Defined in:
lib/mb/plugin_manager.rb

Instance Attribute Summary collapse

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

#initializePluginManager

Returns a new instance of PluginManager.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/mb/plugin_manager.rb', line 28

def initialize
  log.debug { "Plugin Manager starting..." }
  @berkshelf_path = MB::Berkshelf.path
  @plugins        = Set.new

  MB::Berkshelf.init

  async_loading? ? async(:load_all) : load_all

  if eager_loading?
    @eager_load_timer = every(eager_load_interval, &method(:load_all_remote))
  end

  subscribe(ConfigManager::UPDATE_MSG, :reconfigure)
end

Instance Attribute Details

#berkshelf_pathPathname (readonly)

Returns:

  • (Pathname)


18
19
20
# File 'lib/mb/plugin_manager.rb', line 18

def berkshelf_path
  @berkshelf_path
end

#eager_load_timerTimers::Timer? (readonly)

Tracks when the plugin manager will attempt to load remote plugins from the Chef Server. If remote loading is disabled this will return nil.

Returns:

  • (Timers::Timer, nil)


24
25
26
# File 'lib/mb/plugin_manager.rb', line 24

def eager_load_timer
  @eager_load_timer
end

Class Method Details

.instanceCelluloid::Actor(PluginManager)

Returns:

Raises:

  • (Celluloid::DeadActorError)

    if Node Querier has not been started



7
8
9
# File 'lib/mb/plugin_manager.rb', line 7

def instance
  MB::Application[:plugin_manager] or raise Celluloid::DeadActorError, "plugin manager not running"
end

Instance Method Details

#add(plugin, options = {}) ⇒ MB::Plugin?

Add a plugin to the set of plugins

Parameters:

  • plugin (MB::Plugin)
  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :force (Boolean)

    load a plugin even if a plugin of the same name and version is already loaded

Returns:

  • (MB::Plugin, nil)

    returns the set of plugins on success or nil if the plugin was not added



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/mb/plugin_manager.rb', line 53

def add(plugin, options = {})
  if options[:force]
    remove(plugin)
  end

  if find(plugin.name, plugin.version, remote: false)
    return nil
  end

  @plugins.add(plugin)
  plugin
end

#async_change_service_state(service, plugin, environment, state, run_chef = true, options = {}) ⇒ MB::JobTicket

Runs #change_service_state asynchronously

Parameters:

  • service (String)

    a dotted string “component.service_name”

  • plugin (MB::Plugin)

    the plugin currently in use

  • environment (String)

    the environment to operate on

  • state (String)

    the state of the service to change to

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

Returns:



530
531
532
533
534
# File 'lib/mb/plugin_manager.rb', line 530

def async_change_service_state(service, plugin, environment, state, run_chef = true, options = {})
  job = Job.new(:dynamic_service_state_change)
  async(:change_service_state, job, service, plugin, environment, state, run_chef, options)
  job.ticket
end

#async_loading?Boolean

Note:

should be disabled if running motherbrain from the CLIGateway to ensure all plugins are loaded before being accessed

Should the plugin manager perform plugin loading operations in the background?

Returns:

  • (Boolean)


72
73
74
# File 'lib/mb/plugin_manager.rb', line 72

def async_loading?
  Application.config.plugin_manager.async_loading
end

#change_service_state(job, service, plugin, environment, state, run_chef = true, options = {}) ⇒ Object

Parses a service, creates a new instance of DynamicService and executes a Chef run to change the state of the service.

Parameters:

  • job (MB::Job)

    the job to report status on

  • service (String)

    a dotted string “component.service_name”

  • plugin (MB::Plugin)

    the plugin currently in use

  • environment (String)

    the environment to operate on

  • state (String)

    the state of the service to change to

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


550
551
552
553
554
# File 'lib/mb/plugin_manager.rb', line 550

def change_service_state(job, service, plugin, environment, state, run_chef = true, options = {})
  component_name, service_name = service.split('.')
  dynamic_service = MB::Gear::DynamicService.new(component_name, service_name)
  dynamic_service.state_change(job, plugin, environment, state, run_chef, options)
end

#clear_pluginsSet

Clear list of known plugins

Returns:

  • (Set)


79
80
81
# File 'lib/mb/plugin_manager.rb', line 79

def clear_plugins
  @plugins.clear
end

#eager_load_intervalInteger

Note:

to change this option set it in the Config of ConfigManager

The time between each poll of the remote Chef server to eagerly load discovered plugins

Returns:

  • (Integer)


98
99
100
# File 'lib/mb/plugin_manager.rb', line 98

def eager_load_interval
  Application.config.plugin_manager.eager_load_interval
end

#eager_loading?Boolean

Note:

to change this option set it in the Config of ConfigManager

If enabled the plugin manager will automatically discover plugins on the remote Chef Server and load them into the plugin set.

Returns:

  • (Boolean)


89
90
91
# File 'lib/mb/plugin_manager.rb', line 89

def eager_loading?
  Application.config.plugin_manager.eager_loading
end

#find(name, version = nil, options = {}) ⇒ MB::Plugin?

Find and return a registered plugin of the given name and version. If no version attribute is specified the latest version of the plugin is returned.

Parameters:

  • name (String)

    name of the plugin

  • version (#to_s) (defaults to: nil)

    version of the plugin to find

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

    a customizable set of options

Options Hash (options):

  • :remote (Boolean) — default: false

    search for the plugin on the remote Chef Server if it isn’t installed

Returns:



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/mb/plugin_manager.rb', line 147

def find(name, version = nil, options = {})
  options = options.reverse_merge(remote: false)

  return latest(name, options) unless version

  installed = @plugins.find { |plugin| plugin.name == name && plugin.version.to_s == version.to_s }

  return installed if installed

  if options[:remote]
    remote = load_remote(name, version.to_s)
    return remote if remote
  end

  nil
end

#for_environment(plugin_id, environment_id, options = {}) ⇒ MB::Plugin

Determine the best version of a plugin to use when communicating to the given environment

Parameters:

  • plugin_id (String)

    name of the plugin

  • environment_id (String)

    name of the environment

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

    a customizable set of options

Options Hash (options):

  • :remote (Boolean) — default: false

    include plugins on the remote Chef Server which aren’t installed

Returns:

Raises:



178
179
180
181
182
183
184
185
186
# File 'lib/mb/plugin_manager.rb', line 178

def for_environment(plugin_id, environment_id, options = {})
  options = options.reverse_merge(remote: false)
  environment = environment_manager.find(environment_id)
  constraint  = environment.cookbook_versions[plugin_id] || ">= 0.0.0"

  satisfy(plugin_id, constraint, options)
rescue MotherBrain::EnvironmentNotFound => ex
  abort ex
end

#for_run_list_entry(run_list_entry, environment = nil, options = {}) ⇒ MB::Plugin

Finds the plugin for the cookbook specified in the run list entry

Parameters:

  • run_list_entry (String)

    Chef standard run list entry

  • environment (String) (defaults to: nil)

    name of the environment

Returns:



196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/mb/plugin_manager.rb', line 196

def for_run_list_entry(run_list_entry, environment = nil, options = {})
  item = MotherBrain::Chef::RunListItem.new(run_list_entry)
  if item.version
    # version will be defined in run list entries such as
    # recipe[foo::[email protected]], which takes precidence over the
    # environment.
    find(item.cookbook_name, item.version, options)
  elsif !environment.nil?
    for_environment(item.cookbook_name, environment, options)
  else
    find(item.cookbook_name, nil, options)
  end
end

#has_plugin?(name, version) ⇒ Boolean

Parameters:

  • name (String)

    name of the plugin

  • version (#to_s)

    version of the plugin to find

Returns:

  • (Boolean)


214
215
216
# File 'lib/mb/plugin_manager.rb', line 214

def has_plugin?(name, version)
  !find(name, version).nil?
end

#install(name, version = nil) ⇒ MB::Plugin

Download and install the cookbook containing a motherbrain plugin matching the given name and optional version into the user’s Berkshelf.

Parameters:

  • name (String)

    Name of the plugin

  • version (#to_s) (defaults to: nil)

    The version of the plugin to install. If left blank the latest version will be installed

Returns:

Raises:



229
230
231
232
233
234
235
236
# File 'lib/mb/plugin_manager.rb', line 229

def install(name, version = nil)
  unless plugin = find(name, version, remote: true)
    abort MB::PluginNotFound.new(name, version)
  end

  chef_connection.cookbook.download(plugin.name, plugin.version, install_path_for(plugin))
  reload(plugin)
end

#install_path_for(plugin) ⇒ Pathname

The filepath that a plugin would be or should be installed to

Parameters:

Returns:

  • (Pathname)


243
244
245
# File 'lib/mb/plugin_manager.rb', line 243

def install_path_for(plugin)
  Berkshelf.cookbooks_path.join("#{plugin.name}-#{plugin.version}")
end

#installed_versions(name) ⇒ Array<String>

List all installed versions of a plugin with the given name of plugins. An empty array will be returned if no versions of a plugin are installed.

Examples:

plugin_manager.installed_versions("nginx") #=> [ "1.2.3", "2.0.0", "3.1.2" ]

Parameters:

  • name (#to_s)

    name of the plugin

Returns:

  • (Array<String>)


357
358
359
360
361
362
363
364
365
366
# File 'lib/mb/plugin_manager.rb', line 357

def installed_versions(name)
  installed_cookbooks.collect do |path|
    plugin = load_installed(path)
    next unless plugin

    if plugin.name == name
      plugin.version.to_s
    end
  end.compact
end

#latest(name, options = {}) ⇒ MB::Plugin?

Return most current version of the plugin of the given name

Parameters:

  • name (String)

    name of the plugin

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

    a customizable set of options

Options Hash (options):

  • :remote (Boolean) — default: false

    include plugins on the remote Chef server which haven’t been cached or installed

Returns:



256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/mb/plugin_manager.rb', line 256

def latest(name, options = {})
  options = options.reverse_merge(remote: false)

  potentials = list(name: name, remote: false).map(&:version)
  potentials += remote_cookbook_versions(name) if options[:remote]
  potentials = potentials.collect { |version| Semverse::Version.new(version) }.uniq.sort.reverse

  potentials.find do |version|
    found = find(name, version.to_s, options.slice(:remote))
    return found if found
  end

  nil
end

#list(options = {}) ⇒ Array<MB::Plugin>

A set of all the registered plugins

Parameters:

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

    a customizable set of options

Options Hash (options):

  • :name (String)

    filter the results to include only plugins of the given name

  • :remote (Boolean) — default: false

    eargly search for plugins on the remote Chef server and include them in the returned list

Returns:



376
377
378
379
380
381
382
383
384
385
# File 'lib/mb/plugin_manager.rb', line 376

def list(options = {})
  options = options.reverse_merge(remote: false)

  if options[:remote]
    load_all_remote(options.slice(:name))
  end

  result = options[:name].nil? ? @plugins : @plugins.select { |plugin| plugin.name == options[:name] }
  result.sort.reverse
end

#load_allArray<MotherBrain::Plugin>

Returns:



272
273
274
275
# File 'lib/mb/plugin_manager.rb', line 272

def load_all
  load_all_installed
  load_all_remote if eager_loading?
end

#load_all_installed(options = {}) ⇒ Object

Load all of the plugins from the Berkshelf

Parameters:

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

    a customizable set of options

Options Hash (options):

  • :force (Boolean) — default: false


105
106
107
108
109
110
111
# File 'lib/mb/plugin_manager.rb', line 105

def load_all_installed(options = {})
  options = options.reverse_merge(force: false)

  installed_cookbooks.each do |path|
    load_installed(path, options)
  end
end

#load_all_remote(options = {}) ⇒ Object

Load all of the plugins from the remote Chef Server. Plugins with a name and version that have already been loaded will not be loaded again unless forced.

Parameters:

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

    a customizable set of options

Options Hash (options):

  • :name (String)
  • :force (Boolean) — default: false


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/mb/plugin_manager.rb', line 118

def load_all_remote(options = {})
  options = options.reverse_merge(force: false)

  if options[:name].present?
    remote_cookbook_versions(options[:name]).collect do |version|
      load_remote(options[:name], version, options)
    end
  else
    [].tap do |remotes|
      remote_cookbooks.each do |name, versions|
        versions.each { |version| remotes << future(:load_remote, name, version, options) }
      end
    end.map(&:value)
  end
end

#load_installed(path, options = {}) ⇒ MB::Plugin?

Load a plugin from a file

Parameters:

  • path (#to_s)
  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :force (Boolean) — default: true

    load a plugin even if a plugin of the same name and version is already loaded

  • :allow_failure (Boolean) — default: true

    allow for loading failure

Returns:

  • (MB::Plugin, nil)

    returns the loaded plugin or nil if the plugin was not loaded successfully



288
289
290
291
292
293
294
295
# File 'lib/mb/plugin_manager.rb', line 288

def load_installed(path, options = {})
  options = options.reverse_merge(force: true, allow_failure: true)
  load_file(path, options)
rescue PluginSyntaxError, PluginLoadError => ex
  err_msg = "could not load plugin at '#{path}': #{ex.message}"
  options[:allow_failure] ? log.debug(err_msg) : abort(PluginLoadError.new(err_msg))
  nil
end

#load_remote(name, version, options = {}) ⇒ MB::Plugin?

Load a plugin of the given name and version from the remote Chef server

Parameters:

  • name (String)

    name of the plugin to load

  • version (String)

    version of the plugin to load

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

    a customizable set of options

Options Hash (options):

  • :force (Boolean) — default: false

    load a plugin even if a plugin of the same name and version is already loaded

  • :allow_failure (Boolean) — default: true

    allow for loading failure

Returns:

  • (MB::Plugin, nil)

    returns the loaded plugin or nil if the remote does not contain a plugin of the given name and version or if there was a failure loading the plugin

Raises:



315
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
343
344
345
# File 'lib/mb/plugin_manager.rb', line 315

def load_remote(name, version, options = {})
  options  = options.reverse_merge(force: false, allow_failure: true)
  resource = ridley.cookbook.find(name, version)

  return unless resource && resource.has_motherbrain_plugin?

  begin
    scratch_dir   = FileSystem.tmpdir("cbplugin")
     = File.join(scratch_dir, CookbookMetadata::JSON_FILENAME)
    plugin_path   = File.join(scratch_dir, Plugin::PLUGIN_FILENAME)
    lockfile_path = File.join(scratch_dir, Berkshelf::Lockfile::BERKSFILE_LOCK)

    File.write(, resource..to_json)

    unless resource.download_file(:root_file, Plugin::PLUGIN_FILENAME, plugin_path)
      raise PluginLoadError, "failure downloading plugin file for #{resource.name}"
    end

    unless resource.download_file(:root_file, Berkshelf::Lockfile::BERKSFILE_LOCK, lockfile_path)
      log.info "No Berksfile.lock found for #{resource.name} - won't be able to use cookbook versions from lockfile"
    end

    load_file(scratch_dir, options)
  rescue PluginSyntaxError, PluginLoadError => ex
    err_msg = "could not load remote plugin #{name} (#{version}): #{ex.message}"
    options[:allow_failure] ? log.debug(err_msg) : abort(PluginLoadError.new(err_msg))
    nil
  ensure
    FileUtils.rm_rf(scratch_dir)
  end
end

#reload(plugin) ⇒ Object

Remove and Add the given plugin from the set of plugins

Parameters:



390
391
392
# File 'lib/mb/plugin_manager.rb', line 390

def reload(plugin)
  add(plugin, force: true)
end

#reload_allArray<MotherBrain::Plugin>

Reload plugins from Chef Server and from the Berkshelf

Returns:



397
398
399
400
# File 'lib/mb/plugin_manager.rb', line 397

def reload_all
  clear_plugins
  load_all
end

#reload_installedArray<MotherBrain::Plugin>

Reload plugins from the Berkshelf

Returns:



405
406
407
# File 'lib/mb/plugin_manager.rb', line 405

def reload_installed
  load_all_installed(force: true)
end

#remote_versions(name) ⇒ Array<String>

List all versions of a plugin with the given name that are present on the remote Chef server. An empty array will be returned if no versions are present.

Examples:

plugin_manager.remote_versions("nginx") #=> [ "1.2.3", "2.0.0", "3.1.2" ]

Parameters:

  • name (#to_s)

    name of the plugin

Returns:

  • (Array<String>)


509
510
511
512
513
514
515
# File 'lib/mb/plugin_manager.rb', line 509

def remote_versions(name)
  remote_cookbook_versions(name).collect do |version|
    (plugin = load_remote(name, version)).nil? ? nil : plugin.version.to_s
  end.compact
rescue Ridley::Errors::HTTPNotFound
  []
end

#remove(plugin) ⇒ Object

Remove the given plugin from the set of plugins

Parameters:



412
413
414
# File 'lib/mb/plugin_manager.rb', line 412

def remove(plugin)
  @plugins.delete(plugin)
end

#satisfy(plugin_name, constraint, options = {}) ⇒ MB::Plugin

Return the best version of the plugin to use for the given constraint

Parameters:

  • plugin_name (String)

    name of the plugin

  • constraint (String, Semverse::Constraint)

    constraint to satisfy

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

    a customizable set of options

Options Hash (options):

  • :remote (Boolean) — default: false

    include plugins on the remote Chef Server which aren’t installed

Returns:

Raises:

  • (PluginNotFound)

    if a plugin of the given name which satisfies the given constraint is not found



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

def satisfy(plugin_name, constraint, options = {})
  options    = options.reverse_merge(remote: false)
  constraint = Semverse::Constraint.new(constraint)

  # Optimize for equality operator. Don't need to find all of the versions if
  # we only care about one.
  if constraint.operator == "="
    find(plugin_name, constraint.version, options.slice(:remote))
  elsif constraint.to_s == ">= 0.0.0"
    latest(plugin_name, options.slice(:remote))
  else
    graph = Solve::Graph.new
    versions(plugin_name, options[:remote]).each do |version|
      graph.artifact(plugin_name, version)
    end

    solution = Solve.it!(graph, [[plugin_name, constraint]])
    version  = solution[plugin_name]
    # don't search the remote for the plugin again; we would have already done that by
    # calling versions() and including a {remote: true} option.
    find(plugin_name, version, remote: false)
  end
rescue Semverse::NoSolutionError
  abort PluginNotFound.new(plugin_name, constraint)
end

#uninstall(name, version) ⇒ MB::Plugin?

Uninstall an installed plugin

Parameters:

  • name (String)

    Name of the plugin

  • version (#to_s)

    The version of the plugin to uninstall

Returns:



464
465
466
467
468
469
470
471
472
473
# File 'lib/mb/plugin_manager.rb', line 464

def uninstall(name, version)
  unless plugin = find(name, version, remote: false)
    return nil
  end

  FileUtils.rm_rf(install_path_for(plugin))
  remove(plugin)

  plugin
end

#versions(name, remote = false) ⇒ Array<String>

List all of the versions of the plugin of the given name

Parameters:

  • name (#to_s)

    name of the plugin

  • remote (Boolean) (defaults to: false)

    (false) include plugins on the remote Chef server in the results

Returns:

  • (Array<String>)

Raises:

  • (PluginNotFound)

    if a plugin of the given name has no versions loaded



485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/mb/plugin_manager.rb', line 485

def versions(name, remote = false)
  all_versions = installed_versions(name)

  if remote
    all_versions += remote_versions(name)
  end

  if all_versions.empty?
    abort PluginNotFound.new(name)
  end

  all_versions
end