When I started out writing Chef cookbooks, occasionally I'd want to run a knife command from my recipe, library, LWRP or my own gem or knife plugin and I'd typically just use the ruby system method which just creates a subshell to run a command. This never felt quite right. Composing a potentially complex command by building a large string is cumbersome and not politely readable. Then there is the shelling out to a subshell which is inefficient. So after doing some cursory research I was surprised to find little instruction or examples on how to use straight ruby to call knife commands. Maybe my google foo just wasn't up to snuff.
So here I'll run through the basics of how to compose a knife command in ruby, feeding it input and even capturing output and errors.
A simple knife example from ruby
We'll start with a complete but simple example of what a knife call in ruby looks like and then we can dissect it.
# load any dependencies declared in knife plugin Chef::Knife::Ssh.load_deps # instantiate command knife = Chef::Knife::Ssh.new # pass in switches knife.config[:attribute] = 'ipaddress' knife.config[:ssh_user] = "root" knife.config[:ssh_password_ng] = "password" knife.config[:config_file] = Chef::Config[:config_file] # pass in args knife.name_args = ["name:my_node", "chef-client"] # setup output capture stdout = StringIO.new stderr = StringIO.new knife.ui = Chef::Knife::UI.new(stdout, stderr, STDIN, {}) # run the command knife.run puts "Output: #{stdout.string}" puts "Errors: #{stderr.string}"
Setup and create command
This is very straight forward. Knife plugins may optionally define a deps method which is intended to include any require statements needed to load the dependencies of the command. Not all plugins implement this, but you should always call load_deps (which will call deps) just in case they do.
Finally, new up the plugin class. The class name will always reflect the command name where each command name token is capitalized in the class name. So knife cookbook list is CookbookList.
Command input
Knife commands typically take input via two forms:
Normal command line arguments
For instance:
knife cookbook upload my_cookbook
where my_cookbook is an argument to CookbookUpload.
These inputs are passed to the knife command in a simple array via the name_args method ordered just as they would be on the command line.
knife.name_args = ["name:my_node", "chef-client"]
Using our knife ssh example, here we are passing the search query and ssh command.
Command options
These include any command switches defined by either the plugin itself or its knife base classes so it can always include all the standard knife options.
These are passed via the config hash:
knife.config[:attribute] = 'ipaddress' knife.config[:ssh_user] = "root" knife.config[:ssh_password_ng] = "password" knife.config[:config_file] = Chef::Config[:config_file]
Note that the hash keys are usually but not necessarily the same as the name of the option switch so you may need to review the plugin source code for these.
Capturing output and errors
By default, knife commands send output and errors to the STDOUT and STDERR streams using Knife::UI. You can intercept these by providing an alternate UI instance as we are doing here:
stdout = StringIO.new stderr = StringIO.new knife.ui = Chef::Knife::UI.new(stdout, stderr, STDIN, {})
Now instead of logging to STDOUT and STDERR, the command will send this text to our own stdout and stderr StringIO instances. So after we run the command we can extract any output from these instances.
For example:
puts "Output: #{stdout.string}" puts "Errors: #{stderr.string}"
Running the command
This couldn't be simpler. You just call:
knife.run
Hope this is helpful.