Custom Knife Plugins¶
A knife plugin is a set of one (or more) subcommands that can be added to knife to support additional functionality that is not built-in to the base set of knife subcommands. Many of the knife plugins are built by members of the Chef community and several of them are built and maintained by Chef.
The chef-client will load knife and knife plugin commands from the following locations:
- The home directory:
~/.chef/plugins/knife/
- A
.chef/plugins/knife
directory in the cookbook repository - A plugin installed from RubyGems. (For more information about releasing a plugin on RubyGems, see: http://guides.rubygems.org/make-your-own-gem/.)
This approach allows knife plugins to be reused across projects in the home directory, kept in a repository that is accessible to other team members, and distributable to the community using RubyGems.
Syntax¶
There are many ways to structure a knife plugin. The following syntax shows a typical knife plugin:
require 'chef/knife'
# other require attributes, as needed
module ModuleName
class SubclassName < Chef::Knife
deps do
require 'chef/dependency'
# other dependencies, as needed
end
banner "knife subcommand argument VALUE (options)"
option :name_of_option,
:short => "-l VALUE",
:long => "--long-option-name VALUE",
:description => "The description for the option.",
:proc => Proc.new { code_to_run }
:boolean => true | false
:default => default_value
def run
# Ruby code goes here
end
end
where:
require
identifies any other knife subcommands and/or knife plugins required by this pluginmodule ModuleName
declares the knife plugin as its own namespaceclass SubclassName < Chef::Knife
declares the plugin as a subclass ofKnife
, which is in theChef
namespace. The capitalization of this name is important. For example,SubclassName
would have a knife command ofknife subclass name
, whereasSubclassname
would have a knife command ofknife subclassname
deps do
is a list of dependenciesbanner "knife subcommand argument VALUE (options)"
is displayed when a user entersknife subclassName --help
option :name_of_option
defines each of the command-line options that are available for this plugin. For example,knife subclass -l VALUE
orknife subclass --long-option-name VALUE
def run
is the Ruby code that is executed when the command is run
and where for each command-line option:
:short
defines the short option name:long
defines the long option name:description
defines a description that is displayed when a user entersknife subclassName --help
:boolean
defines whether the option istrue
orfalse
; if the:short
and:long
names define aVALUE
, then this attribute must not be used:proc
defines code that determines the value for this option:default
defines a default value
The following example shows part of a knife plugin named knife windows
:
require 'chef/knife'
require 'chef/knife/winrm_base'
class Chef
class Knife
class Winrm < Knife
include Chef::Knife::WinrmBase
deps do
require 'readline'
require 'chef/search/query'
require 'em-winrm'
end
attr_writer :password
banner "knife winrm QUERY COMMAND (options)"
option :attribute,
:short => "-a ATTR",
:long => "--attribute ATTR",
:description => "The attribute to use for opening the connection - default is fqdn",
:default => "fqdn"
... # more options
def session
session_opts = {}
session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug
@session ||= begin
s = EventMachine::WinRM::Session.new(session_opts)
s.on_output do |host, data|
print_data(host, data)
end
s.on_error do |host, err|
print_data(host, err, :red)
end
s.on_command_complete do |host|
host = host == :all ? 'All Servers' : host
Chef::Log.debug("command complete on #{host}")
end
s
end
end
... # more def blocks
end
end
end
Take a look at all of the code for this plugin on GitHub: https://github.com/chef/knife-windows/blob/master/lib/chef/knife/winrm.rb.
Namespace¶
A knife plugin should have its own namespace (even though knife will load a command regardless of its namespace). The namespace is declared using the module
method, for example:
require 'chef/knife'
# other require attributes, as needed
module MyNamespace
class SubclassName < Chef::Knife
where module MyNamespace
declares that the knife plugin has its own namespace, with a namespace of MyNamespace
.
Class Name¶
The class name declares a plugin as a subclass of both Knife
and Chef
. For example:
class SubclassName < Chef::Knife
where SubclassName
is the class name used by this plugin. The capitalization of this name is important. For example, OMG
would have a knife command of knife o m g
, whereas Omg
would have a knife command of knife omg
. Use the capitalization pattern to define the word grouping that best makes sense for the plugin.
A plugin can override an existing knife subcommand by using the same class name as the existing subcommand. For example, to override the current functionality of knife cookbook upload
, use the following class name:
class CookbookUpload < Chef::Knife
Banner¶
A banner displays the syntax for the plugin to users when they enter the --help
option. Use the banner
method in the class body similar to the following:
module example
class example < Chef::Knife
banner "knife example"
...
end
and the when a user enters knife --help
, the following will be displayed:
**EXAMPLE COMMANDS**
knife example
Dependencies¶
The functionality of other knife plugins can be accessed from a plugin by using the deps
method to ensure the necessary files are available. The deps
method acts as a lazy loader, ensuring that dependencies are only loaded into knife when the plugin which requires them is run. Use the following syntax just below the class declaration:
class subclassName < Chef::Knife
deps do
require 'chef/knife/name_of_command'
require 'chef/search/query'
# other dependencies, as needed
end
where the actual path may vary from plugin to plugin, but is typically located in the chef/knife/
directory.
Note
Using the deps
method instead of require
is recommended, especially if the environment in which knife is being run contains a lot of plugins and/or any of those plugins have a lot of dependencies and/or requirements on other plugins and search functionality.
Requirements¶
The functionality of other knife plugins can be accessed from a plugin by using the require
method to ensure the necessary files are available, and then within the code for the plugin, to create a new object of the class of the plugin to be used.
First, ensure that the correct files are available using the following syntax:
require 'chef/knife/name_of_command'
where the actual path may vary from plugin to plugin, but is typically located in the chef/knife/
directory.
Note
Using the deps
method instead of require
is recommended, especially when the environment in which knife is being run contains a lot of plugins and/or any of those plugins have a lot of dependencies and/or requirements on other plugins and search functionality.
For example, use the following to require a plugin named bootstrap
:
require 'chef/knife/bootstrap'
Next, for the required plugin, create an object of that plugin, like this:
bootstrap = Chef::Knife::Bootstrap.new
and then pass arguments or options to that object. This is done by altering that object’s config
and name_arg
variables. For example:
bootstrap.config[:ssh_user] = "myuser"
bootstrap.config[:distro] = "ubuntu10.04-gems"
bootstrap.config[:use_sudo] = true
bootstrap.name_args = "some_host_name"
where the available configuration objects vary from plugin to plugin. Make sure those configuration objects are correct by verifying them in the source files for each plugin.
And then call the object’s run
method, like this:
bootstrap.run
Options¶
Command-line options can be added to a knife plugin using the option
method. An option can have a true/false value:
option :true_or_false,
:short => "-t",
:long => "--true-or-false",
:description => "Is this value true? Or is this value false?",
:boolean => true | false
:default => true
and it can have a string value:
option :some_type_of_string_value,
:short => "-s VALUE",
:long => "--some-type-of-string-value VALUE",
:description => "This is not a random string value.",
:default => 47
and can specify code that is run to determine the option’s value:
option :tags,
:short => "-T T=V[,T=V,...]",
:long => "--tags Tag=Value[,Tag=Value...]",
:description => "A list of tags associated with the virtual machine",
:proc => Proc.new { |tags| tags.split(',') }
where the knife command allows a comma-separated list of values and the :proc
attribute converts that list of values into an array.
When a user enters knife --help
, the description attributes are displayed as part of the help. Using the previous examples, something like the following will be displayed:
**EXAMPLE COMMANDS**
knife example
-s, --some-type-of-string-value This is not a random string value.
-t, --true-or-false Is this value true? Or is this value false?
-T, --tags A list of tags associated with the virtual machine.
When knife runs the command, the options are parsed from the command-line and make the settings available as a hash that can be used to access the config
method. For example, the following option:
option :omg,
:short => '-O',
:long => '--omg',
:boolean => true,
:description => "I'm so excited!"
can be used to update the run
method of a class to change its behavior based on the config
flag, similar to the following:
def run
if config[:omg]
# Oh yeah, we are pumped.
puts "OMG HELLO WORLD!!!1!!11"
else
# meh
puts "I am just a boring example."
end
end
For a knife plugin with the --omg
option, run knife example --omg
to return something like:
OMG HELLO WORLD!!!1!!11
or just knife example
to return:
I am just a boring example.
Arguments¶
A knife plugin can also take command-line arguments that are not specified using the option
flag, for example: knife node show NODE
. These arguments are added using the name_args
method. For example:
banner "knife hello world WHO"
def run
unless name_args.size == 1
puts "You need to say hello to someone!"
show_usage
exit 1
end
who = name_args.first
if config[:omg]
puts "OMG HELLO #{who.upcase}!!!1!!11"
else
puts "Hello, #{who.capitalize}!"
end
end
where
unless name_args.size == 1
is used to check the number of arguments given; the command should fail if the input does not make sensewho = name_args.first
is used to access arguments usingname_args
show_usage
is used to display the correct usage before exiting (if the command fails)
For example, the following command:
$ knife hello world
will return:
You need to say hello to someone!
USAGE: knife hello world WHO
the following command:
$ knife hello world chefs
will return:
Hello, Chefs!
and the following command:
$ knife hello world chefs --omg
will return:
OMG HELLO CHEFS!!!1!!11
config.rb Settings¶
Certain settings defined by a knife plugin can be configured so that they can be set using the config.rb file. This can be done in two ways:
- By using the
:proc
attribute of theoption
method and code that referencesChef::Config[:knife][:setting_name]
- By specifying the configuration setting directly within the
def
Ruby blocks using eitherChef::Config[:knife][:setting_name]
orconfig[:setting_name]
An option that is defined in this manner may be configured using the config.rb file with the following syntax:
knife[:setting_name]
This approach can be useful when a particular setting is used a lot. The order of precedence for a knife option is:
- A value passed via the command line
- A value saved in the config.rb file
- A default value
The following example shows how the knife bootstrap
subcommand checks for a value in the config.rb file by using the :proc
attribute:
option :ssh_port,
:short => "-p PORT",
:long => "--ssh-port PORT",
:description => "The ssh port",
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
where Chef::Config[:knife][:ssh_port]
tells knife to check the config.rb file for a setting named knife[:ssh_port]
.
And the following example shows the knife bootstrap
subcommand calling the knife ssh
subcommand for the actual SSH part of running a bootstrap operation:
def knife_ssh
ssh = Chef::Knife::Ssh.new
ssh.ui = ui
ssh.name_args = [ server_name, ssh_command ]
ssh.config[:ssh_user] = Chef::Config[:knife][:ssh_user] || config[:ssh_user]
ssh.config[:ssh_password] = config[:ssh_password]
ssh.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
ssh.config[:ssh_gateway] = Chef::Config[:knife][:ssh_gateway] || config[:ssh_gateway]
ssh.config[:identity_file] = Chef::Config[:knife][:identity_file] || config[:identity_file]
ssh.config[:manual] = true
ssh.config[:host_key_verify] = Chef::Config[:knife][:host_key_verify] || config[:host_key_verify]
ssh.config[:on_error] = :raise
ssh
end
where
ssh = Chef::Knife::Ssh.new
creates a new instance of theSsh
subclass namedssh
- A series of settings in
knife ssh
are associated withknife bootstrap
using thessh.config[:setting_name]
syntax Chef::Config[:knife][:setting_name]
tells knife to check the config.rb file for various settings- Raises an exception if any aspect of the SSH operation fails
Search¶
Use the Chef server search capabilities from a plugin to return information about the infrastructure to that plugin. Use the require
method to ensure that search functionality is available with the following:
require 'chef/search/query'
Create a search query object and assign it to a variable:
variable_name = Chef::Search::Query.new
After the search object is created it can be used by the plugin to execute search queries for objects on the Chef server. For example, using a variable named query_nodes
a plugin could search for nodes with the webserver
role and then return the name of each node found:
query = "role:webserver"
query_nodes.search('node', query) do |node_item|
puts "Node Name: #{node_item.name}"
end
This result can then be used to edit nodes. For example, searching for nodes with the webserver
role, and then changing the run_list for those nodes to a role named apache2
:
query = "role:webserver"
query_nodes.search('node', query) do |node_item|
ui.msg "Changing the run_list to role[apache2] for #{node_item.name}"
node_item.run_list("role[apache2]")
node_item.save
ui.msg "New run_list: #{node_item.run_list}"
end
It’s also possible to specify multiple items to add to the run_list:
node_item.run_list("role[apache2]", "recipe[mysql]")
And arguments sent with a plugin command can also be used to search. For example, if the command knife envchange "web*"
is sent, then the command will search for any nodes in roles beginning with “web” and then change their environment to “web”:
module MyKnifePlugins
class Envchange < Chef::Knife
banner "knife envchange ROLE"
deps do
require 'chef/search/query'
end
def run
if name_args.size == 1
role = name_args.first
else
ui.fatal "Please provide a role name to search for"
exit 1
end
query = "role:#{role}"
query_nodes = Chef::Search::Query.new
query_nodes.search('node', query) do |node_item|
ui.msg "Moving #{node_item.name} to the web environment"
node_item.chef_environment("web")
node_item.save
end
end
end
User Interaction¶
The ui
object provides a set of methods that can be used to define user interactions and to help ensure a consistent user experience across knife plugins. The following methods should be used in favor of manually handling user interactions:
Method | Description |
---|---|
ui.ask(*args, &block) |
|
ui.ask_question(question, opts={}) |
Use to ask a user the question contained in question . If :default => default_value is passed as the second argument, default_value will be used if the user does not provide an answer. This method will respect the --default command-line option. |
ui.color(string, *colors) |
Use to specify a color. For example, from the server_list = [
ui.color('Instance ID', :bold),
ui.color('Name', :bold),
ui.color('Public IP', :bold),
ui.color('Private IP', :bold),
ui.color('Flavor', :bold),
ui.color('Image', :bold),
ui.color('State', :bold)
]
and from the server = connection.servers.create(server_def)
puts "#{ui.color("Instance ID", :cyan)}: #{server.id}"
puts "#{ui.color("Flavor", :cyan)}: #{server.flavor_id}"
puts "#{ui.color("Image", :cyan)}: #{server.image_id}"
...
puts "#{ui.color("SSH Key", :cyan)}: #{server.key_name}"
print "\n#{ui.color("Waiting for server", :magenta)}"
|
ui.color?() |
Indicates that colored output should be used. (Colored output can only be used when output is sent to a terminal.) |
ui.confirm(question, append_instructions=true) |
Use to ask a Y/N question. If the user responds with N , immediately exit with status code 3. |
ui.edit_data(data, parse_output=true) |
Use to edit data. This opens the $EDITOR. |
ui.edit_object(klass, name) |
|
ui.error |
Use to present an error to the user. |
ui.fatal |
Use to present a fatal error to the user. |
ui.highline |
Use to provide direct access to the Highline object used by many ui methods. |
ui.info |
Use to present a message to a user. |
ui.interchange |
Use to determine if the output is a data interchange format such as JSON or YAML. |
ui.list(*args) |
|
ui.msg(message) |
Use to present a message to the user. |
ui.output(data) |
Use to present a data structure to the user. This method will respect the output requested when the -F command-line option is used. The output will use the generic default presenter. |
ui.pretty_print(data) |
Use to enable pretty-print output for JSON data. |
ui.use_presenter(presenter_class) |
Use to specify a custom output presenter. |
ui.warn(message) |
Use to present a warning to the user. |
For example, to show a fatal error in a plugin in the same way that it would be shown in knife do something similar to the following:
unless name_args.size == 1
ui.fatal "Be sure to say hello to someone!"
show_usage
exit 1
end
Create a Plugin¶
A knife command is a Ruby class that inherits from the Chef::Knife
class. A knife command is run by calling the run
method on an instance of the command class. For example:
module MyKnifePlugins
class HelloWorld < Chef::Knife
def run
puts "Hello, World!"
end
end
end
and is run from the command line using:
$ knife hello world
Exceptions¶
In most cases, the exception handling available within knife is enough to ensure that exception handling for a plugin is consistent with how knife ordinarily behaves. That said, exceptions can also be handled within a knife plugin in the same way they are handled in any Ruby program.
Install a Plugin¶
To install a knife plugin from a file, do one of the following:
- Copy the file to the
~/.chef/plugins/knife
directory; the file’s extension must be.rb
- Add the file to the chef-repo at the
CHEF_REPO/.chef/plugins/knife
; the file’s extension must be.rb
- Install the plugin from RubyGems