Chef: Can a variable set within one ruby_block be used later in a recipe? - ruby

Let's say I have one variable, directory_list, which I define and set in a ruby_block named get_directory_list. Can I use directory_list later on in my recipe, or will the compile/converge processes prevent this?
Example:
ruby_block "get_file_list" do
block do
transferred_files = Dir['/some/dir/*']
end
end
transferred_files.each do |file|
file "#{file}" do
group "woohoo"
user "woohoo"
end
end

Option 1: You could also put your file resource inside the ruby_block.
ruby_block "get_file_list" do
block do
files = Dir['/some/dir/*']
files.each do |f|
t = Chef::Resource::File.new(f)
t.owner("woohoo")
t.group("woohoo")
t.mode("0600")
t.action(:create)
t.run_context=(rc)
t.run_action(:create)
end
end
end
Option 2: You could use node.run_state to pass data around.
ruby_block "get_file_list" do
block do
node.run_state['transferred_files'] = Dir['/some/dir/*']
end
end
node.run_state['transferred_files'].each do |file|
file "#{file}" do
group "woohoo"
user "woohoo"
end
end
Option 3: If this were just one file, you could declare a file resource with action :nothing, look up the resource from within the ruby_block, and set the filename, and then notify the file resource when the ruby_block runs.
Option 4: If this is the example from IRC today, just place your rsync and the recursive chown inside a single bash resource. rsync and chown are already idempotent, so I don't think it's objectionable in this particular case.

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.

YAML Ruby Load multiple environment variables

I inherited a tool that is working correctly but when I try to extend it it just fails. Since I am new to ruby and yaml I dont really know what is the reason why this fails...
So I have a class config that looks like this
class Configuration
def self.[] key
##config[key]
end
def self.load name
##config = nil
io = File.open( File.dirname(__FILE__) + "/../../../config/config.yml" )
YAML::load_documents(io) { |doc| ##config = doc[name] }
raise "Could not locate a configuration named \"#{name}\"" unless ##config
end
def self.[]=key, value
##config[key] = value
end
end
end
raise "Please set the A environment variable" unless ENV['A']
Helpers::Configuration.load(ENV['A'])
raise "Please set the D environment variable" unless ENV['D']
Helpers::Configuration.load(ENV['D'])
raise "Please set the P environment variable" unless ENV['P']
Helpers::Configuration.load(ENV['P'])
So I had a first version with the environment variable A that worked fine, then when I want to integrate 2 more environment variables it fails (they are different key/value sets). I did debug it and it looks like when it reads the second key/value it removes the other ones (such as reading the 3rd removes the previous 2, so I end up with ##config with only the 3rd key/value par instead of all the values I need).
It is probably easy to fix this, any idea how?
Thanks!
EDIT:
The config file use to look like:
Test:
position_x: “56”
position_y: “56”
Now I want to make it like
“x56”:
position_x: “56”
“x15”:
position_x: “15”
“y56”:
position_y: “56”
“y15”:
position_y: “15”
My idea is that I set them separately and I don’t need to create all the combinations…
Each time you call load you delete the previous configuration (in the line ##config = nil). If you want the configuration to be a merger of all files you will want to merge the new configuration to the existing configuration rather than overriding it.
Something like this:
def self.load name
##config ||= {}
io = File.open( File.dirname(__FILE__) + "/../../../config/config.yml" )
YAML::load_documents(io) do |doc|
raise "Could not locate a configuration named \"#{name}\"" unless doc[name]
##config.merge!(doc[name])
end
end
Be aware that if the code was written as it has been because the method was called more than once, and the configuration is expected to reset between reads, you will need to explicitly reset the configuration now:
class Configuration
# ...
def reset_configuration
#config = {}
end
end
Helpers::Configuration.reset_configuration
raise "Please set the A environment variable" unless ENV['A']
Helpers::Configuration.load(ENV['A'])
raise "Please set the D environment variable" unless ENV['D']
Helpers::Configuration.load(ENV['D'])
raise "Please set the P environment variable" unless ENV['P']
Helpers::Configuration.load(ENV['P'])
I'd access the YAML using:
YAML::load_file(File.expand_path("../../../config/config.yml", File.dirname(__FILE__)))
expand_path cleans up the '..' chain and returns the cleaned-up version, relative to FILE. For instance:
foo = '/path/to/a/file'
File.expand_path("../config.yml", File.dirname(foo)) # => "/path/to/config.yml"
load_file reads and parses the entire file and returns it.

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

Create a file in a specified directory

How can I create a new file in a specific directory. I created this class:
class FileManager
def initialize()
end
def createFile(name,extension)
return File.new(name <<"."<<extension, "w+")
end
end
I would like to specify a directory (path) where to create the file. If this one doesn't exist, he will be created. So do I have to use fileutils as shown here just after file creation or can I specify directly in the creation the place where create the file?
Thanks
The following code checks that the directory you've passed in exists (pulling the directory from the path using File.dirname), and creates it if it does not. It then creates the file as you did before.
require 'fileutils'
def create_file(path, extension)
dir = File.dirname(path)
unless File.directory?(dir)
FileUtils.mkdir_p(dir)
end
path << ".#{extension}"
File.new(path, 'w')
end

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