Run arbitrary ruby code in a chef cookbook - ruby

I have a simple chef cookbook and all it does is it sets the MOTD on a CentOS machine. It takes the content of the /tmp/mymotd.txt and turns it into the MOTD.
I also have a simple ruby script (a full-fledged ruby script) that simply reads the text from the web-server and puts in into the /tmp/mymotd.txt.
My questions are:
how do I run this ruby script from within the cookbook?
how do I pass some parameters to the script (e.g. the address of the web-server)
Thanks a lot beforehand.

Ad 1.
You can use libraries directory in scripts to place there your ruby script and declare it in a module. Example:
# includes
module MODULE_NAME
# here some code using your script
# Example function
def example_function (text)
# some code
end
end
You can use then
include MODULE_NAME
in your recipe to import those functions and just use it like
example_function(something)
What's good - you can use there also Chef functions and resources.
IMPORTANT INFO: Just remember that Chef has 2 compilation phases. First will be all of Ruby code, second all of Chef resources. This means, that you have to remember priority of code. I won't write here more info about it, since you haven't asked for this, but if you want, you can find it here.
Ad 2.
You can do this in several ways, but it seems to me, that the best option for you would be to use environments. You can find more info in here. Basically, you can set up environment for script before it will run - this way you can define some variables you would use later.
Hope this helps.

Related

Chef compile error when capturing shell output

I have a chef recipe that looks something like this:
package 'build-essential' do
action :install
end
cmd = Mixlib::ShellOut.new("gcc -dumpversion")
cmd.run_command
gcc_version = cmd.stdout.strip()
If I execute the recipe on a system where gcc is installed, the recipe runs fine without errors. However, if I run the recipe on a system which doesn't have gcc install I get the error 'no such file or directory - gcc'.
I came to know about the chef two-phases stuff when trying to find a solution to my problem. I was expecting the package installation to satisfy the gcc requirement. How can I tell chef that this requirement will be satisfied later and not throw an error at compile time?
I tried the following, but the attribute does not get updated.
Chef::Resource::RubyBlock.send(:include, Chef::Mixin::ShellOut)
ruby_block "gcc_version" do
block do
s = shell_out("gcc -dumpversion")
node.default['gcc_version'] = s.stdout.strip()
end
end
echo "echo #{node[:gcc_version]}" do
command "echo #{node[:gcc_version]}"
end
Any help is appreciated. Thanks.
So okay, a few issues here. First, forget that Chef::Resource::whatever.send(:include trick. Never do it, literally never. In this case, the ShellOut mixin is already available in all the places anyway.
Next, and more importantly, you've still got a two-pass confusion issue. See https://coderanger.net/two-pass/ for details but basically the strings in that echo resource (I assume that said execute originally and you messed up the coping?) get interpolated at compile time. You haven't said what you are trying to do, but you probably need to use the lazy{} helper method.
And last, don't store things in node attributes like that, it's super brittle and hard to work with.

Stub require statement in rspec?

I have to maintain a Ruby script, which requires some libs I don't have locally and which won't work in my environment. Nevertheless I want to spec some methods in this script so that I can change them easily.
Is there an option to stub some of the require statements in the script I want to test so that it can be loaded by rspec and the spec can be executed within my environment?
Example (old_script.rb):
require "incompatible_lib"
class Script
def some_other_stuff
...
end
def add(a,b)
a+b
end
end
How can I write a test to check the add function without splitting the "old_Script.rb" file and without providing the incompatible_lib I don't have?
Instead of stubbing require which is "inherited" from Kernel, you could do this:
Create a dummy incompatible_lib.rb file somewhere that is not in your $LOAD_PATH. I.e., if this is a Ruby application (not Rails), don't put it in lib/ nor spec/.
You can do this a number of ways, but I'll tell you one method: in your spec file which tests Script, modify $LOAD_PATH to include the parent directory of your dummy incompatible_lib.rb.
Ordering is very important -- next you will include script.rb (the file which defines Script).
This will get you around the issue and allow you test test the add method.
Once you've successfully tested Script, I would highly recommend refactoring it so that you don't have to do this technique, which is a hack, IMHO.
Thanks, I also thought about the option of adding the files, but finally hacked the require itself within the test case:
module Kernel
alias :old_require :require
def require(path)
old_require(path) unless LIBS_TO_SKIP.include?(path)
end
end
I know that this is an ugly hack but as this is legacy code executed on a modified ruby compiler I can't easily get these libs running and it's sufficient to let me test my modifications...

what ruby features are used in chef recipes?

I just started using chef and don't know much about ruby.
I have problems understanding the language-syntax used in recipes.
Say, I create a directory in a cookbook in recipes/default.rb like:
directory "/home/test/mydir" do
owner "test"
mode "0755"
action :create
recursive true
end
I assume this is part of a valid ruby script. What do lines like owner "test" mean? Is this a function call, a variable assignment or something else entirely?
Chef is written in Ruby and makes an extensive use of Ruby ability to design custom DSL. Almost every chef configuration file is written with a Ruby-based DSL.
This means that in order to use chef effectively you should be familiar with the basic of Ruby syntax including
Grammar
Data types (the main difference compared to other languages are Symbols)
Blocks
You don't need to know a lot about metaprogramming in Ruby.
The case of the code you posted is an excellent example of a Ruby based DSL. Let me explain it a little bit.
# Call the method directory passing the path and a block
# containing some code to be evaluated
directory "/home/test/mydir" do
# chown the directory to the test user
owner "test"
# set the permissions to 0555
mode "0755"
# create the directory if it does not exists
action :create
# equivalent of -p flag in the mkdir
recursive true
end
Blocks are a convenient way to specify a group of operations (in this case create, set permissions, etc) to be evaluated in a single context (in this case in the context of that path).
Let's break it down.
directory "/home/test/mydir" do
...
end
You are just calling a global method defined by Chef called directory, passing one argument "/home/test/mydir", and a block (everything between the do and end).
This block is probably excecuted in a special scope created by Chef in which all of the options (owner, mode, action, etc.) are method.

Redirect Output of Capistrano

I have a Capistrano deploy file (Capfile) that is rather large, contains a few namespaces and generally has a lot of information already in it. My ultimate goal is, using the Tinder gem, paste the output of the entire deployment into Campfire. I have Tinder setup properly already.
I looked into using the Capistrano capture method, but that only works for the first host. Additionally that would be a lot of work to go through and add something like:
output << capture 'foocommand'
Specifically, I am looking to capture the output of any deployment from that file into a variable (in addition to putting it to STDOUT so I can see it), then pass that output in the variable into a function called notify_campfire. Since the notify_campfire function is getting called at the end of a task (every task regardless of the namespace), it should have the task name available to it and the output (which is stored in that output variable). Any thoughts on how to accomplish this would be greatly appreciated.
I recommend not messing with the Capistrano logger, Instead use what unix gives you and use pipes:
cap deploy | my_logger.rb
Where your logger reads STDIN and STDOUT and both records, and pipes it back to the appropriate stream.
For an alternative, the Engineyard cap recipies have a logger – this might be a useful reference if you do need to edit the code, but I recommend not doing.
It's sort of a hackish means of solving your problem, but you could try running the deploy task in a Rake task and capturing the output using %x.
# ...in your Rakefile...
task :deploy_and_notify do
output = %x[ cap deploy ] # Run your deploy task here.
notify_campfire(output)
puts output # Echo the output.
end

Extraordinarily Simple Ruby Question: Where's My Class?

[I'm just starting with Ruby, but "no question is ever too newbie," so I trudge onwards...]
Every tutorial and book I see goes from Ruby with the interactive shell to Ruby on Rails. I'm not doing Rails (yet), but I don't want to use the interactive shell. I have a class file (first_class.rb) and a Main (main.rb). If I run the main.rb, I of course get the uninitialized constant FirstClass. How do I tell ruby about the first_class.rb?
The easiest way is to put them both in the same file.
However you can also use require, e.g.:
require 'first_class'
You can also use autoload as follows:
autoload :FirstClass, 'first_class'
This code will automatically load first_class.rb as soon as FirstClass is used. Note, however, that the current implementations of autoload are not thread safe (see http://www.ruby-forum.com/topic/174036).
There's another point worth noting: you wouldn't typically use a main file in ruby. If you're writing a command line tool, standard practice would be to place the tool in a bin subdirectory. For normal one-off scripts the main idiom is:
if __FILE__ == $0
# main does here
# `__FILE__` contains the name of the file the statement is contained in
# `$0` contains the name of the script called by the interpreter
#
# if the file was `required`, i.e. is being used as a library
# the code isn't executed.
# if the file is being passed as an argument to the interpreter, it is.
end

Resources