Chef new_resource.<value> vs. <value> - ruby

I inherited a cookbook utilizing another cookbook with a custom resource.
I'm trying to figure out the use of the new_resource in this code.
In the code below, path new_resource.fullpath causes a "non-defined method."
When I changed that line to simply path fullpath to reference the local variable, it worked just fine. I can't figure out what the previous intent was.
I'm sure this is an easy one. Looking for an ELI5. Thanks!
Full code Block.
resource_name :mb_worker_role
default_action :create
property :service_name, String, name_property: true
property :root_path, String, required: true
property :executable, String, required: true
property :wcf_port, [Integer, String], default: 808
property :health_port, [Integer, String], default: 8000
property :use_dummy, [TrueClass, FalseClass], default: true
action :create do
# Create firewall allow for WCF port
windows_firewall_rule "#{new_resource.service_name} - WCF" do
localport new_resource.wcf_port.to_s
protocol 'TCP'
firewall_action :allow
end
# Create firewall allow for health port
windows_firewall_rule "#{new_resource.service_name} - Health" do
localport new_resource.health_port.to_s
protocol 'TCP'
firewall_action :allow
end
# Full path to service executable at root_path\executable
fullpath = "#{new_resource.root_path}\\#{new_resource.executable}"
# Create directory for worker role application root
directory new_resource.root_path do
recursive true
action :create
end
# Set NetTCPPortSharing to start on demand
windows_service 'NetTCPPortSharing' do
action :configure_startup
startup_type :manual
end
# Stage the dummy worker role executable if requested
# Only used to verify this resource when testing
if property_is_set?(:use_dummy)
cookbook_file 'Stage dummy worker role executable' do
# path new_resource.fullpath
path fullpath
cookbook 'mb_worker_role'
source 'WorkerRole.Default.exe'
only_if { new_resource.use_dummy }
action :create
end
end
# Create windows service if it does not exist
powershell_script "Installing Windows service #{new_resource.service_name}" do
code <<-EOH
# Create the Windows service
sc.exe create "#{new_resource.service_name}" binPath="#{fullpath}"
EOH
not_if "(Get-Service -Name #{new_resource.service_name}).Name -eq '#{new_resource.service_name}'"
end
# Set service to automatic and make sure it's running
windows_service new_resource.service_name do
action [:configure_startup, :enable, :start]
startup_type :automatic
end
end

path fullpath is a magical alias system that we actively discourage at this point. It was a nice thing to try, but the syntax has a lot of problems that result in unexpected behavior. Use new_resource.fullpath, far fewer footguns.

Related

List properties of a resource

I'm implementing a custom resource which is basically a facade to an existing resource (in the example below its the vault_certificate resource).
Using the existing resource this code is valid:
certificate = vault_certificate 'a common name' do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
end
template "a path" do
source 'nginx/dummy.conf.erb'
variables(
certificate: certificate.certificate_filename
key: certificate.key_filename
)
end
Notice I can invoke certificate.certificate_filename or certificate.key_filename. Or more generally I can read any property defined by the vault_certificate resource.
Now with the new resource (sort of a facade to vault_certificate)
provides :vault_certificate_handle_exceptions
unified_mode true
property :common_name, String, name_property: true
property :max_retries, Integer, default: 5
action :create do
require 'retries'
# new_resource.max_retries is being used inside the retry_options. I omitted that part as its not relevant for the question
with_retries(retry_options) do
begin
vault_certificate new_resource.common_name do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
ignore_failure :quiet
end
rescue Vault::HTTPClientError => e
data = JSON.parse(e.errors)['data']
if data['error'] == 'Certificate not found locally'
# This error is one we can recover from (actually we are expecting it). This raise with VaultCertificateError will trigger the with_retries.
raise VaultCertificateError.new("Waiting for the certificate to appear in the store (because I'm not the leader)", data)
else
# Any other error means something really went wrong.
raise e
end
end
end
end
If I now use this resource and try to invoke .certificate_filename or .key_filename:
certificate = vault_certificate_handle_exceptions 'a common name' do
action :create
end
template "a path" do
source 'nginx/dummy.conf.erb'
variables(
certificate: certificate.certificate_filename
key: certificate.key_filename
)
end
I get an error saying the method certificate_filename (or key_filename) is not defined for vault_certificate_handle_exceptions. To solve it I resorted to this hack:
provides :vault_certificate_handle_exceptions
unified_mode true
property :common_name, String, name_property: true
property :max_retries, Integer, default: 5
action :create do
require 'retries'
# new_resource.max_retries is being used inside the retry_options. I omitted that part as its not relevant for the question
with_retries(retry_options) do
begin
cert = vault_certificate new_resource.common_name do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
ignore_failure :quiet
end
# These lines ensure we can read the vault_certificate properties as if they were properties of this resource (vault_certificate_handle_exceptions)
Chef::ResourceResolver.resolve(cert.resource_name).properties.keys.each do |name|
new_resource.send(:define_singleton_method, name.to_sym) do
cert.send(name.to_sym)
end
end
rescue Vault::HTTPClientError => e
data = JSON.parse(e.errors)['data']
if data['error'] == 'Certificate not found locally'
# This error is one we can recover from (actually we are expecting it). This raise with VaultCertificateError will trigger the with_retries.
raise VaultCertificateError.new("Waiting for the certificate to appear in the store (because I'm not the leader)", data)
else
# Any other error means something really went wrong.
raise e
end
end
end
end
Is there a cleaner way to achieve this? If not, is there a more direct way to list all the properties of a resource? I thought cert.properties would work, but no luck there.

Use Regex in registry_data_exists Resource in Chef Recipe

I am creating a chef recipe and trying to create/update Registry key using registry_key resource. How I can use regex to validate the presence of the registry key and only update if it is necessary
registry_key'HKLM\\Software\\Microsoft\\WindowsNT\\CurrentVersion\\Winlogon' do
values [{
name: 'SCRemoveOption',
type: :string,
data: '1', }]
# recursive true
action :create
not_if { registry_data_exists?('HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon',{ name: 'SCRemoveOption', type: :string, data: '1' } ,:x86_64) }
end
If the registry key exists with data 1 or 2 or 3, There is no need to udpate or else the registry_key-data should be updated to 1.
not_if { registry_data_exists?('HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon',{ name: 'SCRemoveOption', type: :string, data: '([1-3])'} ,:x86_64) }
(Apologies for previous effort)
I have preferred using a powershell_script resource as I know PS syntax better than ruby/Chef. I used to check with Security Policy was enabled/disabled as follows:-
powershell_script 'my_script' do
guard_interpreter :powershell_script
cwd 'C:\Temp'
code <<-EOH
<...set the registry key(s)...>
EOH
not_if '(get-itemproperty HKLM:\\System\\CurrentControlSet\\Control\\Lsa).DisableDomainCreds -eq "1"'
end
Perhaps you can adapt to make it work with an array or values.

Chef Custom Resources : mutable property or an alternative

I read that as of 2016 OpsCode recommends against LWRP and the alternative HWRP. They rather recommend to use custom resources.
While this makes sense it leaves a lot to be desired in terms of leveraging ruby (unless I've misunderstood?)
I would like to be able to modify an array based on some boolean properties. I can then use that array to pass to the template, as in:
property :get_iptables_info, [TrueClass, FalseClass], default: false
property :get_pkglist, [TrueClass, FalseClass], default: false
property :cw_logs, Array, default: [], required: false
action :create do
ruby_block 'cw_iptables' do
block do
new_resource.cw_logs.push({ "#{new_resource.custom_dir}/iptables/iptables.txt" => { "log_group_name" => new_resource.log_group_name+"/iptables"}})
end
action :run
only_if {new_resource.get_iptables_info}
end
template "my_template" do
variables ({:logstreams => cw_logs})
end
end
Then in my template:
<% #logstreams.each_pair do |path, _object| %>
["#{path}"]
log_group_name = _object["log_group_name"]
<% end %>
The problem is that properties are immutable. So I get the error:
RuntimeError
------------
ruby_block[cw_iptables] (/tmp/kitchen/cache/cookbooks/custom_cw/resources/logs.rb line 43) had an error: RuntimeError: can't modify frozen Array
What's the correct way to do this? What's the correct way to write ruby code within the resource so that it's more modular and uses methods/functions?
Mutating properties inside a custom resource is generally a bad idea (there are exceptions but this isn't one of them). Better would be to use a local variable. Related, you don't need to use a ruby_block like that when you're already in a custom resource's action:
action :create do
cw_logs = new_resource.cw_logs
if new_resource.get_iptables_info
cw_logs += [whatever extra stuff you want]
end
template "my_template" do
variables logstreams: cw_logs
end
end
Notably that's using += instead of push which doesn't mutate the original. If you used push you would want cw_logs = new_resource.cw_logs.dup or similar.

How do you update an environment variable using Ruby and Chef?

I know how to set the variables for both user and machine.
The problem arises when I try to add to the PATH. Currently my code will overwrite what is in the PATH.
execute 'set java_home2' do
command "setx -m PATH2 \"D:\\Home"
*only_if {"PATH2" == " "}*
end
This currently ensures that the PATH will only run if there is no PATH. When the only_if is removed the problem of overwriting arises.
EDIT:
I am now able to modify the system variable but cannot work out how to do the same with the user variables
env 'path addition' do
key_name "PATH"
value (ENV["PATH"] + ";D:\\Home\\Apps\\variable")
:modify
end
From the question, it looks like you are trying to add PATH on windows server. In that case you can use windows cookbook resource called windows_path for such operation:
windows_path 'C:\Sysinternals' do
action :add
end
https://github.com/chef-cookbooks/windows
https://supermarket.chef.io/cookbooks/windows
I can't speak for specifics in chef, but in ruby, you can access environment variables with the ENV hash. So for PATH, you could do the following:
ENV["PATH"] = ENV["PATH"].split(":").push("/my/new/path").join(":")
That will update your PATH for the duration of the program's execution. Keep in mind that:
This will only update PATH for your ruby script, and only temporarily. Permanently changing your PATH is more complicated and dependent on OS.
This code assumes you're using linux. In windows, the PATH delimiter is ; instead of :, so you should update the code accordingly.
I found the answer:
#Append notepad to user PATH variable
registry_key "HKEY_CURRENT_USER\\Environment" do
$path_name = ""
subkey_array = registry_get_values("HKEY_CURRENT_USER\\Environment", :x86_64)
subkey_array.each{ |val|
case val[:name].include?("PATH")
when true
$path_name = val[:data]
print "\n The User PATH is: #{$path_name}"
break
when false
print ':'
end
}
values [{
:name => "PATH",
:type => :string,
:data => "#{$path_name};D:\\Home\\Apps\\Notepad++\\Notepad++"
}]
action :create
#add a guard to prevent duplicates
not_if {
$path_name.include?("D:\\Home\\Apps\\Notepad++\\Notepad++")
}
end
This code when ran from the CMD line will print the current User PATH variables, then it will append D:/Home/Apps/Notepad++/Notepad++ IF it is not currently in the PATH. If it already exists then this will be skipped.

Chef Foodcritic rule not catching attribute strings

I have written a foodcritic rule to catch any attempt to write to a blacklist of directories/files under the /etc directory.
When blacklisted paths are passed to resource declarations as strings in a recipe, the rule triggers, however when they are passed as attributes, the rule does not trigger:
#resources = [
'file',
'template',
'remote_file',
'remote_directory',
'directory'
]
#blacklist = [
'/etc/ssh/',
'/etc/init',
...
]
rule 'RULE001', 'do not manipulate /etc other than init/,init.d/ & default/' do
tags %w(security)
recipe do |ast|
violations = []
#resources.each do |resource_type|
violations << find_resources(ast, type: resource_type).select do |resource|
res_str = (resource_attribute(resource, 'path' || resource_name(resource)).to_s
#blacklist.any? { |cmd| res_str.include? cmd }
end
end
violations.flatten
end
end
Testing this using the below, the literal strings are caught, however when passed as attributes they are passed. Can anyone see what I'm missing?
attributes/default.rb:
default['testbook']['etc-test'] = '/etc/ssh/test.conf'
default['testbook']['etc-dir-test'] = 'etc/ssh/somedir/'
recipes/default.rb:
#template '/etc/ssh/test.conf' do <-- caught
template node['testbook']['etc-test'] do #<-- not caught
source 'test.conf'
owner 'nobody'
group 'nobody'
mode '0644'
action :create
end
#directory '/etc/ssh/somedir' do <-- caught
directory node['testbook']['etc-dir-test'] do <-- not caught
action :create
end
Yes, this isn't something you can fully handle via static analysis. Foodcritic and tools like it can only handle things that are static in the code, anything that could vary at runtime won't be known.

Resources