Creating a common method for Capistrano tasks - ruby

Let's say in my standard deploy.rb file I have a set of namespaces. I have a common task that lists RPM packages based on a variable I pass to it. When I run this as is, it complains about capture being an undefined method. If I include that method inside the deploy.rb file, it works just fine.
Mind you, I'm new to ruby and to OOP so I'm sure I'm doing this the wrong way. :-)
deploy.rb
load 'config/module'
namespace :lp_app do
desc "LP tasks"
co = Tasks::Commands.new()
task :list do
co.list_pkg("LP")
end
end
module.rb
module Tasks
class Commands
def list_pkg(component)
File.open("#{component}.file.list", "r").each_line do |line|
pkg_name = "#{line}".chomp
set :server_pkg, capture("rpm -q #{pkg_name}")
puts "#{server_pkg}"
end
end
end
end

You are trying to use Capistano specific commands outside of Capistrano. If you want to set a variable to the result of something you run on the command line, try the backtick (`).
module Tasks
class Commands
def list_pkg(component)
File.open("#{component}.file.list", "r").each_line do |line|
pkg_name = "#{line}".chomp
server_pkg = `rpm -q #{pkg_name}`
puts server_pkg
end
end
end
end

Related

Chef - access new_resource from library

Is there a way I can access new_resource attributes from inside a Chef library (in libraries/default.rb) ?
My current solution is:
In libraries/default.rb
module Libraries
def init(resource)
##server_name = resource.server_name
##server_type = resource.server_type
##script = get_script_path
...
end
def get_script_path
if ##server_type == 'admin'
script = 'admin_cntl.sh'
path = '/admin_server/bin'
elsif ##server_type == 'managed'
script = 'managed_cntl.sh'
path = '/managed_server/bin'
end
::File.join(path, script)
end
end
In providers/default.rb
include Libraries
action :start do
init(new_resource)
execute 'my_script' do
command "./#{##script} start"
end
end
action :remove do
init(new_resource)
execute 'my_script' do
command "./#{##script} stop"
end
end
I think this is unnecessary overhead but I couldn't come up with a better solution.
Is there a better way ?
Use a normal mixin:
# libraries/default.rb
module MyLibrary
def script_path
case new_resource.server_type
when 'admin'
'/admin_server/bin/admin_cntl.sh'
when 'managed'
'/managed_server/bin/managed_cntl.sh'
end
end
end
# providers/default.rb
include MyLibrary
action :start do
execute 'my_script' do
command "./#{script_path} start"
end
end
action :remove do
execute 'my_script' do
command "./#{script_path} stop"
end
end
Also remember you can define methods directly in the provider if they are only useful for that one provider.

How to extract tasks and variables from a Rakefile?

I need to:
Open a Rakefile
Find if a certain task is defined
Find if a certain variable is defined
This works to find tasks defined inside a Rakefile, but it pollutes the global namespace (i.e. if you run it twice, all tasks defined in first one will show up in the second one):
sub_rake = Rake::DefaultLoader.new
sub_rake.load("Rakefile")
puts Rake.application.tasks
In Rake, here is where it loads the Makefile:
https://github.com/ruby/rake/blob/master/lib/rake/rake_module.rb#L28
How do I get access to the variables that are loaded there?
Here is an example Rakefile I am parsing:
load '../common.rake'
#source_dir = 'source'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']
Here's some things I tried that didn't work. Using eval on the Rakefile:
safe_object = Object.new
safe_object.instance_eval("Dir.chdir('" + f + "')\n" + File.read(folder_rakefile))
if safe_object.instance_variable_defined?("#staging_dir")
puts " Staging directory is " + f.yellow + safe_object.instance_variable_get("#staging_dir").yellow
else
puts " Staging directory is not specified".red
end
This failed when parsing desc parts of the Rakefile. I also tried things like
puts Rake.instance_variables
puts Rake.class_variables
But these are not getting the #source_dir that I am looking for.
rakefile_body = <<-RUBY
load '../common.rake'
#source_dir = 'some/source/dir'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']
RUBY
def source_dir(ast)
return nil unless ast.kind_of? AST::Node
if ast.type == :ivasgn && ast.children[0] == :#source_dir
rhs = ast.children[1]
if rhs.type != :str
raise "#source_dir is not a string literal! #{rhs.inspect}"
else
return rhs.children[0]
end
end
ast.children.each do |child|
value = source_dir(child)
return value if value
end
nil
end
require 'parser/ruby22'
body = Parser::Ruby22.parse(rakefile_body)
source_dir body # => "some/source/dir"
Rake runs load() on the Rakefile inside load_rakefile in the Rake module. And you can easily get the tasks with the public API.
Rake.load_rakefile("Rakefile")
puts Rake.application.tasks
Apparently that load() invocation causes the loaded variables to be captured into the main Object. This is the top-level Object of Ruby. (I expected it to be captured into Rake since the load call is made in the context of the Rake module.)
Therefore, it is possible to access instance variables from the main object using this ugly code:
main = eval 'self', TOPLEVEL_BINDING
puts main.instance_variable_get('#staging_dir')
Here is a way to encapsulate the parsing of the Rakefile so that opening two files will not have all the things from the first one show up when you are analyzing the second one:
class RakeBrowser
attr_reader :tasks
attr_reader :variables
include Rake::DSL
def task(*args, &block)
if args.first.respond_to?(:id2name)
#tasks << args.first.id2name
elsif args.first.keys.first.respond_to?(:id2name)
#tasks << args.first.keys.first.id2name
end
end
def initialize(file)
#tasks = []
Dir.chdir(File.dirname(file)) do
eval(File.read(File.basename(file)))
end
#variables = Hash.new
instance_variables.each do |name|
#variables[name] = instance_variable_get(name)
end
end
end
browser = RakeBrowser.new(f + "Rakefile")
puts browser.tasks
puts browser.variables[:#staging_dir]

Spec Testing a Ruby CLI

I am trying to test the first ruby CLI i've written (n00b alert) and need some help. All my code is within 1 file, this includes a Class, OptionParser and some basic class execution methods. Here's an idea of what that looks like
The *rb. file
require 'optparse'
require 'fileutils'
class Foo
attr_accessor :arg, :opt
def initialize(p={})
#opt = p[:opt] || false
end
def do_something(arg)
#arg = arg
end
#more methods...
end
# Options
#options={}
#opt_parser = OptionParser.new do |opt|
opt.banner = "<{ FooBar }>"
opt.separator "------------"
opt.on("-o", "--opt", "An Option" do
#options[:opt] = true
end
end
#opt_parser.parse!
#CLI Execution
#foo = Foo.new(#options)
#foo.do_something(ARGV[0])
So here is the problem, i know would like to run some rspec tests rspec spec/ that i've wrote for the class, however the lines outside the class get executed of course and im left with an ARGV error.
What im looking for
Is there a better way to organize my code so i can test all the pieces, or how could i write a test to accommodate this file, Any and all suggestions would be greatly appreciated.
One posible solution is to wrap your option parsing code with a conditional that checks if the file is being run directly or loaded by some other file.
if __FILE__ == $0
# option parsing code
end
If you do that then all the code inside the if __FILE__ == $0 will not run with your test, but the rest of the code will run normally.

Embed RSpec test in a Ruby class

I often build little single-purpose Ruby scripts like this:
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
end
w = Widget.new
puts w.render_data(w.end_data)
__END__
data set to work on.
I'd like to include RSpec tests directly inside the file while I'm working on it. Something like this (which doesn't work but illustrates what I'm trying to do):
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
def self_test
# This doesn't work but shows what I'm trying to
# accomplish. The goal is to have RSpec run these type
# of test when self_test is called.
describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
end
end
w = Widget.new
w.self_test
__END__
data set to work on.
I understand this is not the normal way to work with RSpec and isn't appropriate in most cases. That said, there are times when it would be nice. So, I'd like to know, is it possible?
There are two things. First off rspec by default won't pollute the global namespace with methods like describe and so on. The second thing is that you need to tell rspec to run the specs after they've been declared.
If you change your self_test method to be
RSpec.describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
RSpec::Core::Runner.invoke
(having of course done require 'rspec' then that will run your specs).
The invoke methods exits the process after running the specs. If you don't want to do that, or need more control over where output goes etc. you might want to drop down to the run method which allows you to control these things.

DRY within a Chef recipe

What's the best way to do a little DRY within a chef recipe? I.e. just break out little bits of the Ruby code, so I'm not copying pasting it over and over again.
The following fails of course, with:
NoMethodError: undefined method `connect_root' for Chef::Resource::RubyBlock
I may have multiple ruby_blocks in one recipe, as they do different things and need to have different not_if blocks to be truley idempotent.
def connect_root(root_password)
m = Mysql.new("localhost", "root", root_password)
begin
yield m
ensure
m.close
end
end
ruby_block "set readonly" do
block do
connect_root node[:mysql][:server_root_password] do |connection|
command = 'SET GLOBAL read_only = ON'
Chef::Log.info "#{command}"
connection.query(command)
end
end
not_if do
ro = nil
connect_root node[:mysql][:server_root_password] do |connection|
connection.query("SELECT ##read_only as ro") {|r| r.each_hash {|h|
ro = h['ro']
} }
end
ro
end
end
As you already figured out, you cannot define functions in recipes. For that libraries are provided. You should create a file (e.g. mysql_helper.rb ) inside libraries folder in your cookbook with the following:
module MysqlHelper
def self.connect_root( root_password )
m = Mysql.new("localhost", "root", root_password)
begin
yield m
ensure
m.close
end
end
end
It must be a module, not a class. Notice also we define it as static (using self.method_name). Then you will be able to use functions defined in this module in your recipes using module name with method name:
MysqlHelper.connect_root node[:mysql][:server_root_password] do |connection|
[...]
end
For the record, I just created a library with the following. But that seems overkill for DRY within one file. I also couldn't figure out how to get any other namespace for the module to use, to work.
class Chef
class Resource
def connect_root(root_password)
...

Resources