Chef - access new_resource from library - ruby

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.

Related

Rspec. The tested code is automatically started after test

I have a problem with the testing the Sensu Plugin.
Everytime when I start rspec to test plugin it test it, but anyway at the end of test, the original plugin is started automatically. So I have in my console:
Finished in 0 seconds (files took 0.1513 seconds to load)
1 example, 0 failures
CheckDisk OK: # This comes from the plugin
Short explanation how my system works:
Plugin call system 'wmic' command, processes it, checks the conditions about the disk parameters and returns the exit statuses (ok, critical, etc)
Rspec mocks the response from system and sets into the input of plugin. At the end rspec checks the plugin exit status when the mocked input is given.
My plugin looks like that:
require 'rubygems' if RUBY_VERSION < '1.9.0'
require 'sensu-plugin/check/cli'
class CheckDisk < Sensu::Plugin::Check::CLI
def initialize
super
#crit_fs = []
end
def get_wmic
`wmic volume where DriveType=3 list brief`
end
def read_wmic
get_wmic
# do something, fill the class variables with system response
end
def run
severity = "ok"
msg = ""
read_wmic
unless #crit_fs.empty?
severity = "critical"
end
case severity
when /ok/
ok msg
when /warning/
warning msg
when /critical/
critical msg
end
end
end
Here is my test in Rspec:
require_relative '../check-disk.rb'
require 'rspec'
def loadFile
#Load template of system output when ask 'wmic volume(...)
end
def fillParametersInTemplate (template, parameters)
#set mocked disk parameters in template
end
def initializeMocks (options)
mockedSysOutput = fillParametersInTemplate #loadedTemplate, options
po = String.new(mockedSysOutput)
allow(checker).to receive(:get_wmic).and_return(po) #mock system call here
end
describe CheckDisk do
let(:checker) { described_class.new }
before(:each) do
#loadedTemplate = loadFile
def checker.critical(*_args)
exit 2
end
end
context "When % of free disk space = 10 >" do
options = {:diskName => 'C:\\', :diskSize => 1000, :diskFreeSpace => 100}
it 'Returns ok exit status ' do
begin
initializeMocks options
checker.run
rescue SystemExit => e
exit_code = e.status
end
expect(exit_code).to eq 0
end
end
end
I know that I can just put "exit 0" after the last example, but this is not a solution because when I will try to start many spec files it will exit after the first one. How to start only test, without running the plugin? Maybe someone can help me and show how to handle with such problem?
Thank you.
You can stub the original plugin call and optionally return a dummy object:
allow(SomeObject).to receive(:method) # .and_return(double)
you can put it in the before block to make sure that all assertions will share the code.
Another thing is that you are using rescue blocks to catch the situation when your code aborts with an error. You should use raise_error matcher instead:
expect { run }.to raise_error(SystemExit)

how to share a rake variable in the code it invokes?

What I need is basically send a target argument and use it in my RSpec tests, e.g.:
$ rake unistall_and_run[test_spec.rb]
Rakefile:
desc 'uninstall app to run tests'
task :uninstall_and_run, [:arg] do |t, arg|
#note this, i will explain later
start_driver(fullReset: true)
oi = arg.to_s.split('"')[1]
file_dir = (project_home + '/spec/' + oi)
exec "rspec #{file_dir}"
end
start_driver is called on that line, but when I run the tests (exec "rspec ..."), it is called again and the args I passed is overwritten by the default (because its on RSpec config).
What I'd like to do is, on my RSpec file check if it was already called and don't run again.
Here is the start_driver method:
def start_driver(options= {})
if options.empty?
capabilities = caps
else
capabilities = caps(options)
end
$appium = Appium::Driver.new(caps: capabilities)
$browser = $appium.start_driver
Appium.promote_appium_methods RSpec::Core::ExampleGroup
end
So, i found a way to do it. Its not beautiful though. I save a file with the args I want when run rake:
desc 'uninstall app to run tests'
task :uninstall_and_run, [:arg] do |t, arg|
send_custom_caps(fullReset: true)
oi = arg.to_s.split('"')[1]
file_dir = (project_home + '/spec/' + oi)
exec "rspec #{file_dir}"
end
and the send_custom_caps method is:
def send_custom_caps(*opts)
file = File.new(custom_caps_file, "w+")
File.open(file, 'w') do |f|
f.write(opts)
end
end
now the ruby code itself (in this case, my spec config) will check if there is custom args before start_driver. Here is my custom start_driver method (which I renamed):
def start_appium_driver (options= {})
if options.empty?
get_caps
if $custom_args
capabilities = caps($custom_args)
else
capabilities = caps
end
else
capabilities = caps(options)
end
$appium = Appium::Driver.new(caps: capabilities)
$browser = $appium.start_driver
Appium.promote_appium_methods RSpec::Core::ExampleGroup
end
and get_caps
def get_caps
if File.exist?(custom_caps_file) #$custom_args
file = File.read(custom_caps_file)
$custom_args = eval(file)
File.delete (custom_caps_file)
end
$custom_args unless $custom_args.defined?
end
probably this is not the best solution, but it is working ok for me :)

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]

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)
...

Creating a common method for Capistrano tasks

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

Resources