Chef recipe node hash doesn't have the same data as in chef server node data - ruby

I have a conditional in a recipe that checks for some node data:
if node[:etc][:group].has_key?('someuser') and node[:etc][:group][:someuser][:gid] == 12345
# do something...
else
# do something else...
end
And I used knife node edit hostname.mydomain.com to add this data to my node object, as shown by
$ knife node show hostname.mydomain.com -m
Node Name: hostname.mydomain.com
Environment: ...
FQDN: hostname.mydomain.com
IP: ...
Run List: ...
Roles: ...
Recipes: ...
Platform: ...
Tags: ...
Attributes:..
etc:
group:
someuser:
gid: 12345
members:
tags: ...
However, the else clause is being executed instead of the if clause. I checked that this is the version of the recipe that is being run on the node (looked at the recipe that was downloaded to the node). I thought I had set up my node correctly to execute the if clause so why is the else clause executing? Thanks.

The node etc key is created by ohai (and is under a automatic key) and rewritten at each run, trying to set thoose attributes in node object won't work.
You can't override automatic attributes.
You can have more details on what is done on a chef run here and details about attributes here
What you can do here is creating the group in your recipe with the group resource and reload ohai data before entering the loop.
Exemple (heavily inspired from the doc ):
ohai "reload_passwd" do
action :nothing
plugin "etc"
end
group 'somegroup' do
gid 12345
notifies :reload, "ohai[reload_passwd]", :immediately
end
ruby_block "your code" do # in a ruby block to be run at converge time and not at compile time.
if node[:etc][:group].has_key?('someuser') and node[:etc][:group][:someuser][:gid] == 12345
# do something...
else
# do something else...
end
end
But the group creation could be in your else block, and next chef run will enter the if block as the group will exist.

Related

What is the best way to get UID from user within Chef?

I am using "user" resource to define the user within Chef.
Now, I want to retrieve the value of UID and GID for the same user.
Can this be done using the ruby code within chef?
Right now, I am trying to use bash resource and running the following command:
id -u $USERNAME
You can use the automatic attributes contributed by Ohai.
That information is accessible via
# uid
node['etc']['passwd']['$USERNAME']['uid']
# gid
node['etc']['passwd']['$USERNAME']['gid']
From the command line, you can explore the attributes as follows:
$ ohai etc/passwd/vagrant
{
"dir": "/home/vagrant",
"gid": 900,
"uid": 900,
"shell": "/bin/bash",
"gecos": "vagrant,,,"
}
$ ohai etc/passwd/vagrant/uid
900
If you create a user during the chef run and want to access its information within the same chef run, you probably have to trigger a reload of the responsible ohai plugin. (It might be possible that triggers this automatically, but I wouldn't expect so.)
ohai 'reload passwd' do
plugin 'passwd'
action :reload
end
user 'john' do
action :reload, 'ohai[reload passwd]'
end
For this you can use Ruby's Etc class: http://ruby-doc.org/stdlib-2.1.2/libdoc/etc/rdoc/Etc.html#method-c-getpwnam
[15] pry(main)> Etc.getpwnam('david').uid
=> 501
[16] pry(main)> Etc.getpwnam('david').gid
=> 20

How to set Chef Vault variable in recipe

Below is the command used to create the vault
knife vault create ldapuser user1 -A "admin,chef1.example.com" -J password.json -M client
Below is the command that shows content of the vault
knife vault show ldapuser user1
id: user1
password: secretp#ssword
username: james
Below is my recipe which includes the following at the very top
chef_gem 'chef-vault' do
compile_time true if respond_to?(:compile_time)
action :install
end
require 'chef-vault'
item = ChefVault::Item.load("ldapuser","user1")
execute 'setup ldap' do
command '/opt/ldap/bin/setup --hostname localhost --port 389 --bindDN cn="Directory Manager" --bindPassword item[password] --ldapport 389 --baseDN "dc=example,dc=com" --addBaseEntry --doNotStart --acceptLicense`
end
execute 'run ldap' do
command '/opt/ldap/bin/ldapmodify --hostname localhost --port 389 --bindDN cn="Directory Manager" --bindPassword item[password] --filename /opt/ldap.ldif
end
Unfortunately once setup is complete and i try to log into my ldap server, i get an invalid credentials error message.
I assume it has to do with how the variable for the bindPassword is defined in the execute block. I even tried logging in using item['password'] and that didnt work. However when i hard code the password (instead of using the vault) into my recipe, i am able to login without any issues.
I have searched everywhere and can't seem to find a solution that works. Please help!
String interpolation in Ruby looks like this: "something #{item['key']} else".
Important bits: use double quotes not single, put #{} around the expression, and make sure you correctly format the expression inside the #{}.
In this example I'm populating a auth.properties and a pem file from secrets from the chef-vault.
Full YouTube Demo Here: How to Create and use Chef-Vault
Default.rb recipe
chef_gem 'chef-vault' do
compile_time true if respond_to?(:compile_time)
end
require 'chef-vault'
secrets = ChefVault::Item.load("vault_demo", "dev_secrets" )
directory "/opt/your_project" do
action :create
end
template '/opt/your_project/auth.properties' do
source "auth.properties.erb"
variables({
sql_password: secrets['sql_password'],
application_password: secrets['application_password']
})
action :create
end
template '/opt/your_project/server.pem' do
source "server.pem.erb"
variables({
ssl_cert: Base64.decode64(secrets['ssl_cert'])
})
action :create
end
Here are the templates:
auth.properties.erb
ssl_password:<%= #sql_password %>
application_password:<%= #application_password %>
server.pem.erb
<%= #ssl_cert %>
Note that the pem file is being base64 decoded in the recipe because it has to be encoded to store in the vault

How can I structure my playbook to make more sense?

Currently, I have the following playbook:
- hosts: "{{env}}_{{product}}"
name: "tools"
sudo: yes
vars_prompt:
product: "Which product would you like to deploy [all|proxy|query|rest]?"
env: "Which environment should we deploy to [dev|qa|test|prod]?"
roles:
- { role: proxy, when: "product == 'all' or product == 'proxy'" }
- { role: query, when: "product == 'all' or product == 'query'" }
- { role: rest, when: "product == 'all' or product == 'rest'" }
All of my groups are stored in a single ./inventory/all file, e.g.,:
# -----------------------------------------------------------------------------------
# --------------
# ### DEV ###
# --------------
# -----------------------------------------------------------------------------------
[dev_all:children]
dev_redis
dev_query
dev_rest
dev_proxy
[dev_redis]
.hosts
[dev_query]
.hosts
[dev_rest]
.hosts
[dev_proxy:children]
dev_proxy-dc1
dev_proxy-dc2
dev_proxy-dc3
[dev_proxy-dc1]
.hosts
[dev_proxy-dc2]
.hosts
[dev_proxy-dc3]
.hosts
# -----------------------------------------------------------------------------------
# --------------
# ### PROD ###
# --------------
# -----------------------------------------------------------------------------------
[prod_all:children]
prod_redis
prod_query
prod_rest
prod_proxy
[prod_redis]
.hosts
[prod_query]
.hosts
[prod_rest]
.hosts
[prod_proxy:children]
prod_proxy-dc1
prod_proxy-dc2
prod_proxy-dc3
[prod_proxy-dc1]
.hosts
[prod_proxy-dc2]
.hosts
[prod_proxy-dc3]
.hosts
I can't help but feel like I'm making this overly complex though. I'm trying to avoid requiring people to pass in tags or inventory files. But now I'm not sure what the best way to go about allowing deploys for things like redis hosts, which aren't really what we would consider a "product", but still requires it's own group since it's hosted on it's own set of hosts. I could just add it to the current list, [all|proxy|query|rest|redis], but... it seems like there should be a way to specify redis and another product, but at the same time not requiring both... I don't know how though.
I wanted to have something where you could say
"I want to deploy proxy to dev, and let's update redis while we're at it"
-- is this possible with my current setup?
This doesn't feel like the best way to handle this.
Instead, I'd structure my playbooks individually to target specific roles and I'd equally set my inventories to only cover a certain environment rather than everything.
If you want to make it so that people don't have to pass in the inventory file or the specific playbook then I'd wrap the ansible-playbook command(s) in some form of wrapper script.
This allows people to use your playbooks a lot more granularly and flexible as you need it but still offer the same ability to offer presets through your wrapper script.

SaltStack: edit yaml file on minion host based on salt pillar data

Say the minion host has a default yaml configuration named myconf.yaml. What I want to do is to edit parts of those yaml entries using values from a pillar. I can't even begin to think how to do this on Salt. The only think I can think of is to run a custom python script on the host via cmd.run and feed it with input via arguments, but this seems overcomplicated.
I want to avoid file.managed. I cannot use a template, since the .yaml file is big, and can change by external means. I just want to edit a few parameters in it. I suppose a python script could do it but I thought salt could do it without writing s/w
I have found salt.states.file.serialize with the merge_if_exists option, I will try this and report.
You want file.serialize with the merge_if_exists option.
# states/my_app.sls
something_conf_file:
file.serialize:
- name: /etc/my_app.yaml
- dataset_pillar: my_app:mergeconf
- formatter: yaml
- merge_if_exists: true
# pillar/my_app.sls
my_app:
mergeconf:
options:
opt3: 100
opt4: 200
On the target, /etc/my_app.yaml might start out looking like this (before the state is applied):
# /etc/my_app.yaml
creds:
user: a
pass: b
options:
opt1: 1
opt2: 2
opt3: 3
opt4: 4
And would look like this after the state is applied:
creds:
user: a
pass: b
options:
opt1: 1
opt2: 2
opt3: 100
opt4: 200
As far as I can tell this uses the same algorithm as pillar merges, so e.g. you can merge or partially overwrite dictionaries, but not lists; lists can only be replaced whole.
This can be done for both json and yaml with file.serialize. Input can be inline on the state or come from a pillar. A short excerpt follows:
state:
cassandra_yaml:
file:
- serialize
# - dataset:
# concurrent_reads: 8
- dataset_pillar: cassandra_yaml
- name: /etc/cassandra/conf/cassandra.yaml
- formatter: yaml
- merge_if_exists: True
- require:
- pkg: cassandra-pkgs
pillar:
cassandra_yaml:
concurrent_reads: "8"

Chef:Install windows_feature with guard clause not_if

I'm using Chef 12.4 and using chef-client in local mode. I'm trying to install windows features and detect their current status before I do the installation, or removal. The reason for using the not if, is because when using DISM it takes for ever to check the current state.
Currently I have a server that takes 45 minutes to build and then 20 minutes to run chef even when there is nothing to do.
I used the following Powershell script 'service-check.ps1' to output the status of the package, this is put in place by the recipe itself:
$content = Get-Content .\features.txt
$feature = $args[0]
function grep($f) {
foreach($_ in $content){
$data = $_.split("|")
%{if($($data[0]) -match $f) {return "$($data[1])"}}
}
}
grep $feature
The recipe I'm using writes the 'features.txt' file to the location where Chef is being run, by querying dism for the current features and their status. The recipe file is as follows:
#Check the features and their current state
powershell_script 'installed_features' do
code <<-EOH
dism /online /get-features /format:table > c://chef//features.txt
EOH
end
cookbook_file 'C:\Scripts\service-check.ps1' do
source 'service-check.ps1'
end
windows_feature 'TelnetClient' do
action :remove
only_if {
status = powershell_out("Powershell -f C:\\scripts\\service-check.ps1 TelnetClient").stdout.chop
Disabled != status
}
end
I have tried many different formats in the only_if {} segment, however none of them have worked. I used the example that is here:
http://pdalinis.blogspot.co.uk/2013/09/chef-using-powershell-as-conditional.html
The output I get is:
* windows_feature[TelnetClient] action remove
================================================================================
Error executing action `remove` on resource 'windows_feature[TelnetClient]'
================================================================================
NameError
---------
uninitialized constant Chef::Recipe::Disabled
Resource Declaration:
---------------------
# In C:/chef/local-mode-cache/cache/cookbooks/tester/recipes/default.rb
12: windows_feature 'TelnetClient' do
13: action :remove
14: only_if {
15: status = powershell_out("Powershell -f C:\\scripts\\service-check.ps1 TelnetClient").stdout.chop
16: Disabled != status
17: }
18: end
Compiled Resource:
------------------
# Declared in C:/chef/local-mode-cache/cache/cookbooks/tester/recipes/default.rb:12:in `from_file'
windows_feature("TelnetClient") do
provider LWRP provider windows_feature_dism from cookbook windows
action [:remove]
retries 0
retry_delay 2
default_guard_interpreter :default
declared_type :windows_feature
cookbook_name "tester"
recipe_name "default"
only_if { #code block }
end
I'm a novice at Chef, Ruby and Powershell. I'm assuming that the only_if or not_if are expecting a true or false result, however this seems to be appending the written status that I've added the check against (in this script, disabled).
I have checked the powershell script, it does indeed output Enabed / Disabled depending on the status.
My questions are:
Is it possible to use windows_feature and powershell_out in this way, according to the example it's using powershell_script, but I'd rather use the windows_feature specification to install features.
If it is possible, what am I doing wrong? I have a feeling it might be the output of the script as it seems to insert a space:
" Enabled"
" Disabled"
If it isn't possible, is there another way to do it?
I'd be very grateful for any assistance.
Thank you.
is it possible you just need to quote "Disabled"? Otherwise Ruby is trying to figure out what it is, but doesn't know about any variable or class with that name.

Resources