Chef builtin search method change - ruby

I use the AWS-ApplyChefRecipes document in SSM to run Chef cookbooks against my instances and manage the users, SSH keys, and sudo permissions for each instance based on tags. AWS on October 7 appear to have upgraded their Chef client implementation from version 14.14.29 to version 14.15.6. This update introduced breaking changes and I have not been able to get this resolved.
This recipe uses Ohai to get the tags from the instance, put them into an array, and then loop through each tag, search for the user JSON files inside the directory data_bags/<tag>/<user>.json it then goes through each of the groups listed and will either create/update a user, remove a user, or lock a user based on the information in the <user>.json file. The code below was working until October 7.
print "\n"
print node['ec2']['tags']['chef-users']
print "\n"
instanceTag = node['ec2']['tags']['chef-users']
tagArray = instanceTag.split(" ")
print "tagArray = "
print tagArray
print "\n"
for tag in tagArray do
print "tag = "
print tag
print "\n"
userList = search(tag, '*:*')
users_manage "sudonopw" do
print "entering users_manage sudonopw\n"
users userList
action [:remove, :create, :lock]
end
users_manage "users" do
print "entering users_manage users\n"
users userList
action [:remove, :create, :lock]
end
users_manage "admin" do
print "entering users_manage admin\n"
users userList
action [:remove, :create, :lock]
end
end rescue NoMethodError
print "Users ended, starting sudo\n"
sudo "sudonopw" do
group "sudonopw"
nopasswd true
end
sudo "admin" do
group "admin"
end
I have been troubleshooting this for several days now and have narrowed it down to this line:
userList = search(tag, '*:*')
This was previously returning the array of user JSON, however, after implementing some error checking:
begin
print "getting userList\n"
userList = search(tag, '*:*')
rescue => exception
print "something happened here\n"
print exception.message
print "\n"
else
print "userlist = "
print userList
print "\n"
ensure
print "done getting userList\n"
end
I now get the following output indicating that the search is not returning results any longer.
tag = admin
getting userList
something happened here
404 "Not Found"
done getting userList
Looking through the commits, I have not seen anything that should break my implementation.
https://github.com/chef/chef/blob/bc7687e56763cedbd010cfd566aa2fc0c53bb194/lib/chef/search/query.rb

Related

Ruby: How to make program return nil if there is nothing in location of table

In Ruby I have this csv file:
make,model,color,doors,email
dodge,charger,black,4,practice1#whatever.com
ford,focus,blue,5,practice2#whatever.com
nissan,350z,black,2,practice3#whatever.com
mazda,miata,white,2,practice4#whatever.com
honda,civid,brown,4,practice5#whatever.com
corvette,stingray,red,2,practice6#whatever.com
ford,fiesta,blue,5,practice7#whatever.com
bmw,m4,black,2,practice8#whatever.com
audi,a5,blue,2,practice9#whatever.com
subaru,brz,black,2,practice10#whatever.com
lexus,rc,black,2,practice11#whatever.com
and this is my code:
def delete_from_data
print "\nTo delete a car, enter the email (this is case sensitive): \n> "
delete_car = gets.chomp
#this allows me to delete a certain row based on email
table = CSV.table("cars.csv")
table.delete_if do |row|
row[:email] == delete_car
end
File.open("cars.csv", "w") do |f|
f.write(table.to_csv)
end
#this shows the updated student roster after deleting user
puts "\nThis is the updated roster after deleting: #{delete_car}"
end
Having this code, how can I make it if the user enters an invalid email (misspelled email or anything that doesn't match an email from the CSV file), it asks to enter a valid one since the one they typed isn't an email from the CSV file
Load table first.
Now you can use #any? to see if any rows contain that email address.
table.any? { |row| row[:email] == delete_car }
Now, just do this in a loop, breaking the loop and returning the input email when the input email address is in the table.
delete_car = loop do
email = gets.chomp
break email if table.any? { |row| row[:email] == email }
puts "Invalid input. Try again."
end
You may also wish to generate a set of valid email addresses. If you think you'll need to loop several times, this may lead to better performance.
valid_emails = table.map { |row| row[:email] }.to_set
delete_car = loop do
email = gets.chomp
break email if valid_emails.include?(email)
puts "Invalid input. Try again."
end

Allowing a user to edit a CSV file

In Ruby 3.1.2, using this CSV file:
make,model,color,doors,email
dodge,charger,black,4,practice1#whatever.com
ford,focus,blue,5,practice2#whatever.com
nissan,350z,black,2,practice3#whatever.com
mazda,miata,white,2,practice4#whatever.com
honda,civid,brown,4,practice5#whatever.com
corvette,stingray,red,2,practice6#whatever.com
ford,fiesta,blue,5,practice7#whatever.com
bmw,m4,black,2,practice8#whatever.com
audi,a5,blue,2,practice9#whatever.com
subaru,brz,black,2,practice10#whatever.com
lexus,rc,black,2,practice11#whatever.com
If the user does not enter an email address that is part of the CSV file above or anything that does not equal an email, the program asks the user to enter a valid email until they actually do, and then update the CSV file with the deleted row corresponding to the email. My current code does this but there is no checking if the user enters a valid email address or not and that is the part I am stuck on. This is my code:
def delete_from_data
print "\nTo delete a car, enter the email (this is case sensitive): \n> "
delete_car = gets.chomp
#this allows me to delete a certain row based on email
table = CSV.table("cars.csv")
table.delete_if do |row|
row[:email] == delete_car
end
File.open("cars.csv", "w") do |f|
f.write(table.to_csv)
end
#this shows the updated student roster after deleting user
puts "\nThis is the updated roster after deleting: #{delete_car}"
end
for example, if the user enters "practice11#whatever.com", then that whole row from the CSV file gets deleted. But if a user enters anything else other than a valid email address from the CSV file, it still runs.
You should read the documentation of CSV::Table and notice that it has a #each method that allows you to iterate over the rows. In fact, lots of Ruby container classes have a #each method so you will need to get familiar with it if you want to do anything meaningful in Ruby.
Something like this should work:
email_found = false
while !email_found
email = gets.chomp
table.each do |row|
if row[:email] == email
email_found = true
end
end
end
Note that this is not meant to be beautiful or optimized code, but simple, beginner-level code.
For extra credit, learn about Ruby's Enumerable module and use .any? instead of .each.

Search for a .yml file in a folder

I have developed a system where a specific user has access to search for a specific file. The problem is that this file is in a folder so I am having a hard time coding it.
My method must be able to find my keyword input in the file name.
For example, If I search for Bob, I will get all the files where Bob are included in the filename
def search_user(search)
keyword = File.readlines('test3.yml')
matches = keyword.select { |username| username[/#{search}/] }
if File.read("test3.yml").include?(search)
puts "_____________________________________________"
puts ("Search results for student: " + search + ":") #
puts
puts matches
puts "_____________________________________________"
else #If not it will give the user feedback that its not there
puts "_____________________________________________"
puts
puts ("Sorry, we couldnt find #{search} in the system.")
puts "_____________________________________________"
end
end

How to print all linux users with Puppet and ruby?

I want to create a facter that returns all users.
Facter.add("sysusers") do
setcode do
File.readlines('/etc/passwd').each do |line|
line.match(/^[^:]+/)[0]
end
end
end
Then in my .pp file I have this:
$users = inline_template("<%= scope.lookupvar('sysusers') %>")
$users.each |String $user| {
notify { "$user":}
}
This should work but the facter returns just one letter at a time.
So notify { "$user":} just prints:
Notify[r]
Notify[o]
And then it craches because the next letter is also "o" (two o`s in "root" and root is the first user stated in /etc/passwd).
So how can I print all the users?
EDIT
With the edit to:
Facter.add("sysusers") do
setcode do
File.readlines('/etc/passwd').each do |line|
line.match(/^[^:]+/).to_s
end
end
end
Then the output is:
root#mymachine]# facter sysusers
[
"root:x:0:0:root:/root:/bin/bash
",
"bin:x:1:1:bin:/bin:/usr/bin/nologin
",
"daemon:x:2:2:daemon:/:/usr/bin/nologin
...
...
So it still does not seem to work as expeced.
This is the match you want.
line.match(/^[^:]+/).to_s
When you add [0], it is taking the first character from the string that is the user name.
EDIT
File.readlines('/etc/passwd').collect do |line|
line.match(/^[^:]+/).to_s
end
That will collect an array which to be returned in your setcode.
Parsing /etc/passwd is a clunky approach to your problem.
It's cleaner to use the Etc module
require 'etc'
result = []
Etc.passwd { |user| result << user.name }
result
Use the following ruby code, which reads and prints the user names from /etc/passwd file.
IO.readlines("/etc/passwd").each do |val|
user = val.split(":").first
puts user
end

Reading a file and creating a user in Chef

I have a list of IP address along with me. In front of those IP I have a username. What I am trying to do is make Chef read the file having IP and username and once it encounter the IP, it should create a user of that name.
But when I do I get a user but the name of the user comes out to be a number.
Here is my recipe
File.open("/tmp/users.txt", "r") do |file|
file.readlines.each_with_index do |ip,user|
if ip = node[:ipaddress]
user ip[user] do
action :create
supports :manage_home => true
comment 'Test User'
home '/home/ip[user]'
shell '/bin/bash'
password 'password'
end
end
end
my users.txt file
231.27.59.232, test1
272.27.59.15, tes2
985.54.25.22, test3
Now when I run the recipe this is what I get
Recipe: repo_update::users
* cookbook_file[/tmp/users.txt] action create (up to date)
* user[1] action create
- create user 1
* user[7] action create
- create user 7
* user[2] action create
- create user 2
Please tell me what is wrong here.
Lots of problem here... The answer of Tejay is the way to go, I'll just try to explain why your code don't work and how to fix it so it could be of some use later :)
File.open("/tmp/users.txt", "r") do |file|
file.readlines.each_with_index do |ip,user|
puts "values are #{ip} and #{user}"
end
end
Gives:
values are 231.27.59.232, test1
and 0
values are 272.27.59.15, tes2
and 1
values are 985.54.25.22, test3
and 2
each_with_index won't split magically your line into two part, it will just assign the last parameter the actual index in the iteration.
A fixed version of your code would be:
File.open("/tmp/users.txt", "r") do |file|
file.readlines.each do |line| # just iterate and get line
ip,myuser=line.gsub("\n",'').split(',') # set ip and myuser variable with values comma separated, using myuser to avoid conflict with the resource name. Using gsub to remove traling carriage return in user name
if ip == node[:ipaddress] # test equality, a single = will assign ip a value and always be true.
user myuser do # create the user using the variable, no need to interpolate here
action :create
supports :manage_home => true
comment 'Test User'
home "/home/#{myuser}" # use interpolation here inside double quotes (won't work in single quotes)
shell '/bin/bash'
password 'password'
end
end
end
end
The problem is this line:
user ip[user] do
You are calling the [] method on the ip string. Furthermore, you're going to get a name collision between the resource user and the block variable. Finally, you are giving each user the home of '/home/ip[user]'. You need to put the string in "'s and wrap the variable in #{ and } Try this:
File.open("/tmp/users.txt", "r") do |file|
file.readlines.each do |line|
entries = line.split(',')
ip = entries[0].strip
username = entries[1].strip
if ip = node[:ipaddress]
user username do
action :create
supports :manage_home => true
comment 'Test User'
home "/home/#{username}"
shell '/bin/bash'
password 'password'
end
end
end
Also, reading this all from a file is a very non cheffy thing to do. Either use a databag or a hash stored in an environment variable, which also saves you from needing to loop at all:
userhash = node['my_users'][node['ipadddress']]
user userhash['username']
action :create
supports :manage_home => true
comment 'test user'
home userhash['home'] || "/home/#{userhash['username']"
shell userhash['shell'] || '/bin/bash'
password userhash['password'] || 'password'
end

Resources