Commit 0f425342 authored by Alex Tayts's avatar Alex Tayts
Browse files

Merge branch 'adamhl' into 'master'

Merge changes from adamhl's refactoring of tools scripts

See merge request !1
parents fa60ab11 a46c35e6
[[_TOC_]]
Helper scripts for Users module
-------------------------------
Ruby scripts to add and remove users from "users" module hirea datasource. POSIX attributes
Ruby scripts to add and remove users from "users" module hiera datasource. POSIX attributes
of a user account are matching ones in LDAP. Ruby has been chosen because it does not require
installation of any additional stuff (modules, packages, etc.) on a Mac PAW.
......@@ -16,19 +18,96 @@ installation of any additional stuff (modules, packages, etc.) on a Mac PAW.
Add a user
```
./add_user.rb sunet_id
./add_user sunet_id
```
Delete user:
```
./del_user.rb sunet_id
./del_user sunet_id
```
List users:
```
./list_users sunet_id
```
Refresh users:
```
./refresh_users sunet_id
```
The above are all wrapper scripts around `manage-user` and take the same
options as `manage-user`. For more options, see the section "Advanced
options" below.
## Notes
* The script also determines if a user has a base or fully sponsored SUNet account.
* The script also determines if a user has a base or fully sponsored SUNet account.
If a user has a base account, i.e. no AFS space, the script forces his home directory
in hiera data source to be set to /home/sunet_id. Users module relies on this
information to automatically put base account homes locally and create .k5login
files for them.
## Configuration
You can set script configuration using command-line options or by using
the file `manage-user.yml`. The file `manage-user.yml` _must_ be in the
same directory as the `manage-user` script. The configuration options you
can set are `classname`, `git-commit`, and `verbose`. Example:
```
---
# ./manage-user.yml
# Recognized configuration options:
#
# - classname
# - git-commit
# - verbose
# Where to find the user data:
classname: defaults::iedo_users
# Set git-commit to true to do a git auto-commit, false otherwise:
git-commit: false
# Set verbose to true to get more verbose output:
verbose: true
```
Note: If you have an option in the `manage-user.yml` configuration file
_and_ you set the same configuration option via a command-line option, the
command-line option setting will be the one used.
## Advanced options
By default, the scripts assume that the YAML file where the
user information is stored is in `modules/users/data/common.yaml` in this format:
```
users::stanford_users:
adamhl:
uid: 12345
gid: 37
comment: Adam Henry Lewenberg
home: "/afs/ir/users/a/d/adamhl"
shell: "/bin/tcsh"
...
```
If you keep this file in a _different_ location, use the `classname` configuration setting in
`manage-user.yml` (or the `-c` command-line option). For example:
```
$ ./add-user adamhl -c defaults::iedo_users
```
will look for the file `modules/defaults/data/common.yaml` which should have the format
```
defaults::iedo_users:
adamhl:
uid: 12345
gid: 37
comment: Adam Henry Lewenberg
home: "/afs/ir/users/a/d/adamhl"
shell: "/bin/tcsh"
...
```
#!/bin/bash
./manage-user add $@
#!/usr/bin/env ruby
# Adds a user to users::stanford_users hash in "users" module hiera
# common.yaml data source.
#
# NOTE: Since the user data in hiera is a hash, output of the tool
# is not sorted. Every time the script is used, the data
# source is rewritten with the user list in a different order.
#
# NOTE: The code assumes that the script is located in "tools" subdirectory
# in the root of the repo.
require 'yaml'
require 'pathname'
if ARGV.empty?
puts 'Usage: add_user sunet_id'
exit
end
sunetid = ARGV.shift
# determine where the root of the repo is
# assuming that this script is always in "tools" subdirectory
# of ther repo's root.
repo_root = Pathname.new(__dir__).parent()
# figure out a path to the data file
data_source = repo_root.join('modules/users/data/common.yaml')
unless File.exists?(data_source)
puts "Unable to locate data source"
puts "at #{data_source.to_s}."
exit
end
# read the contents of the hiera data source
hiera_data = YAML.load_file(data_source)
# check if the user already exists in the data source
if hiera_data['users::stanford_users'].has_key?(sunetid)
puts "The user #{sunetid} is already known."
exit
end
# lookup a u information in lsdb
account = `remctl lsdb user show #{sunetid}`
# if a remctl command failed, then there is no ticket or remctl
# client is not installed.
unless $?.success?
puts "Please make sure remctl is installed, you have a valid kerberos ticket,"
puts "and you have the rights to execute remctl lsdb queries."
exit
end
# if sunetid is not found
if account.include? "not found in database"
puts "#{sunetid} is not a valid SUnet ID."
exit
end
# create a hash with all user attributes
u = Hash.new
account.each_line do |line|
attrib, val = line.strip.split(/:\s*/, 2)
next if val.nil? or val.empty?
u[attrib] = val unless u.has_key?(attrib)
end
# add user attributes from passwd string
u['pwd'], u['uid'], u['gid'], u['gecos'], u['home'], u['shell'] = u[sunetid].split(/:/)
# if there is no attribute 'Services' or there is no 'afs' value there
# then the user has a base sponsored sunet id or inactive
unless u.has_key?('Services') and u['Services'].include?('afs')
u['home'] = "/home/#{sunetid}"
end
# add user to hiera data source
hiera_data['users::stanford_users'][sunetid] = {
'uid' => u['uid'],
'gid' => u['gid'],
'comment' => u['gecos'],
'home' => u['home'],
'shell' => u['shell']
}
# save data source as yaml
File.open(data_source, 'w') {|f| f.write hiera_data.to_yaml }
# commit the change to git, watch for errors
if system('git add ' + data_source.to_s)
unless system('git commit -m "Add a user \"' + sunetid + '\" to users module data source."')
puts "Error committing the change to git. Please do it manually."
exit
end
else
puts "Error adding the data source file to git. Please add and commit manually."
exit
end
puts <<-EOS
User "#{sunetid}" has been successfully added. The commit has not yet been
pushed to the remote. To complete this change, run:
git push
EOS
#!/bin/bash
./manage-user delete $@
#!/usr/bin/env ruby
# Deletes a user from users::stanford_users hash in "users" module hiera
# common.yaml data source.
#
# NOTE: Since the user data in hiera is a hash, output of the tool
# is not sorted. Every time the script is used, the data
# source is rewritten with the user list in a different order.
#
# TODO: Sort the user hash when output to the file.
require 'yaml'
require 'pathname'
if ARGV.empty?
puts 'Usage: del_user sunet_id'
exit
end
sunetid = ARGV.shift
# determine where the root of the repo is
# assuming that this script is always in "tools" subdirectory
# of ther repo's root.
repo_root = Pathname.new(__dir__).parent()
# figure out a path to the data file
data_source = repo_root.join('modules/users/data/common.yaml')
unless File.exists?(data_source)
puts "Unable to locate data source"
puts "at #{data_source.to_s}."
exit
end
# read the contents of the hiera data source
hiera_data = YAML.load_file(data_source)
# check if the user already exists in the data source
if hiera_data['users::stanford_users'].has_key?(sunetid)
just_deleted = hiera_data['users::stanford_users'].delete(sunetid)
# puts 'Deleted the following data from hiera:'
# puts just_deleted.to_yaml
else
puts "The user #{sunetid} is not in hiera data. Nothing to do."
exit
end
# save data source as yaml
File.open(data_source, 'w') {|f| f.write hiera_data.to_yaml }
# commit the change to git, watch for errors
if system('git add ' + data_source.to_s)
unless system('git commit -m "Delete a user \"' + sunetid + '\" from the users module data source."')
puts "Error committing the change to git. Please do it manually."
exit
end
else
puts "Error adding the data source file to git. Please add and commit manually."
exit
end
puts <<-EOS
User "#{sunetid}" has been successfully deleted. The commit has not yet been
pushed to the remote. To complete this change, run:
git push
EOS
#!/bin/bash
./manage-user list $@
#!/usr/bin/env ruby
# Adds a user to users::stanford_users hash in "users" module hiera
# common.yaml data source.
#
# NOTE: Since the user data in hiera is a hash, output of the tool
# is not sorted. Every time the script is used, the data
# source is rewritten with the user list in a different order.
#
# NOTE: The code assumes that the script is located in "tools" subdirectory
# in the root of the repo.
require 'yaml'
require 'pathname'
require 'optparse'
def exit_with_error(msg)
puts "error: #{msg}"
exit 1
end
def exit_without_error(msg)
puts msg
exit 0
end
########################################################################
########################################################################
class ManageUser
attr_accessor :verbose
attr_accessor :dryrun
attr_accessor :hiera_data
attr_accessor :git_commit
attr_accessor :data_source
attr_accessor :classname
def initialize()
@verbose = false
@dryrun = true
@data_source = nil
@git_commit = false
@classname = 'users::stanford_users'
@hiera_data = nil
@action = nil
@config_file = File.join(__dir__, 'manage-user.yml')
self.read_configuration()
end
def progress(msg)
if (@verbose) then
puts "[progress] #{msg}"
end
end
def dryrun_progress(msg)
puts "[dry run] #{msg}"
end
def read_configuration()
config_data = YAML.load_file(@config_file)
if (config_data.key?('classname')) then
self.classname = config_data['classname']
end
if (config_data.key?('git-commit')) then
self.git_commit = config_data['git-commit']
end
if (config_data.key?('verbose')) then
self.verbose = config_data['verbose']
end
end
def set_data_source()
# Determine where the root of the repo is assuming that this script is
# always in "tools" subdirectory of the repo's root.
repo_root = Pathname.new(__dir__).parent()
self.progress("repo_root is #{repo_root}")
# figure out a path to the data file. We want the _first_ part of
# the class name.
prefix_regex = %r{^(?<prefix>[^:]+)::(.*)$}
if (matches = @classname.match(prefix_regex)) then
classname_prefix = matches[:prefix]
self.progress("classname prefix is #{classname_prefix}")
else
exit_with_error("could not determine class prefix for '#{@classname}'")
end
@data_source = repo_root.join("modules/#{classname_prefix}/data/common.yaml")
self.progress("data_source is #{@data_source}")
unless File.exist?(@data_source)
exit_with_error("Unable to locate data source at #{@data_source.to_s}")
end
self.set_hiera_data()
return @data_source
end
def set_hiera_data()
@hiera_data = YAML.load_file(@data_source)
end
def user_already_known?(sunetid)
return @hiera_data[@classname].key?(sunetid)
end
def get_user_info_from_lsdb(sunetid)
# lookup a user's information in lsdb via a remctl call.
account = `remctl lsdb user show #{sunetid}`
rv = $?
self.progress("remctl return status is '#{rv}'")
self.progress("remctl returned '#{account}'")
# if the remctl command failed, then there is no ticket or remctl
# client is not installed.
unless rv.success?
msg = 'Please make sure remctl is installed, you have a valid kerberos ticket,
and you have the rights to execute remctl lsdb queries.'
exit_with_error(msg)
end
# if sunetid is not found
if (account.include?('not found in database')) then
exit_with_error("#{sunetid} is not a valid SUnet ID.")
end
return account
end
def add_user(sunetid)
# check if the user already exists in the data source
if (self.user_already_known?(sunetid)) then
exit_with_error("The user #{sunetid} is already known.")
end
# lookup a user's information in lsdb via a remctl call.
account = self.get_user_info_from_lsdb(sunetid)
# create a hash with all user attributes
u = Hash.new
account.each_line do |line|
attrib, val = line.strip.split(/:\s*/, 2)
next if val.nil? or val.empty?
u[attrib] = val unless u.key?(attrib)
end
self.progress("u is #{u}")
# add user attributes from passwd string
u['pwd'], u['uid'], u['gid'], u['gecos'], u['home'], u['shell'] = u[sunetid].split(/:/)
# if there is no attribute 'Services' or there is no 'afs' value there
# then the user has a base sponsored sunet id or inactive
unless u.key?('Services') and u['Services'].include?('afs')
u['home'] = "/home/#{sunetid}"
end
# add user to hiera data source
@hiera_data[@classname][sunetid] = {
'uid' => u['uid'].to_i(),
'gid' => u['gid'].to_i(),
'comment' => u['gecos'],
'home' => u['home'],
'shell' => u['shell']
}
self.save('add', sunetid)
end
def delete_user(sunetid)
# check if the user already exists in the data source
if @hiera_data[@classname].key?(sunetid)
just_deleted = @hiera_data[@classname].delete(sunetid)
self.progress('Deleted the following data from hiera:')
self.progress(just_deleted.to_yaml)
else
exit_without_error("The user #{sunetid} is not in the hiera data. Nothing to do.")
end
self.save('delete', sunetid)
end
def refresh_data()
# Iterate over each user.
sunetids = []
@hiera_data[@classname].each do |sunetid_data|
sunetid = sunetid_data[0]
sunetids.push(sunetid)
end
sunetids.each do |sunetid|
self.progress("refreshing #{sunetid}")
self.delete_user(sunetid)
self.add_user(sunetid)
end
end
def list_data()
# Iterate over each user.
sunetids = []
@hiera_data[@classname].each do |sunetid_data|
sunetid = sunetid_data[0]
data = sunetid_data[1]
uid = data['uid']
print <<~HEREDOC
#{sunetid}:
uid: #{data['uid']}
gid: #{data['gid']}
comment: #{data['comment']}
home: #{data['home']}
shell: #{data['shell']}
HEREDOC
end
end
def make_git_commit(action, sunetid)
if (@git_commit) then
if (@dryrun) then
self.dryrun_progress("committing to git")
else
# commit the change to git, watch for errors
# construct git commit message.
if (action == 'add') then
msg = "added user #{sunetid} to"
elsif (action == 'delete') then
msg = "deleted user #{sunetid} from"
elsif (action == 'refresh') then
msg = "refreshed all users in"
else
exit_with_error("cannot do a git commit with action '#{action}'")
end
commit_msg = "#{msg} users module data source (#{@classname})."
if system('git add ' + @data_source.to_s)
unless system(%Q[git commit -m "#{commit_msg}"])
exit_with_error('Error committing the change to git. Please do it manually.')
end
else
exit_with_error('Error adding the data source file to git. Please add and commit manually.')
end
puts <<-EOS
User "#{sunetid}" has been successfully added. The commit has not yet been
pushed to the remote. To complete this change, run:
git push
EOS
end
else
# Skipping git commit
self.progress("skipping git commit")
end
return
end
def write_data_source_file()
if (@dryrun) then
dryrun_progress("writing changes to '#{@data_source}'")
else
File.open(@data_source, 'w') {|f| f.write @hiera_data.to_yaml }
end
end
def save(action, sunetid)
self.write_data_source_file()
self.make_git_commit(action, sunetid)
end
end
########################################################################
########################################################################
## # #### # #### # #### # #### # #### # #### # #### # #### # #### # ####
### Argument and option parsing
ARGV << '-h' if ARGV.empty?
options = {}
optparse = OptionParser.new do |opts|
opts.banner = "Usage: manage-user (add|delete) <sunet_id> [-v] [-c classname]\n" \
' manage-user (list|refresh) [-v] [-c classname]'
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
options[:verbose] = v
end
opts.on('-n', '--[no-]dry-run', 'Do a dry-run') do |n|
options[:dryrun] = n
end
opts.on('-g', '--[no-]git-commit', 'Commit the changes to git (default: no commit)') do |g|
options[:gitcommit] = g
end
class_default = 'users'
opts.on('-c', '--classname CLASSNAME',
"-c Puppet class where users are defined (defaults to '#{class_default}')") do |lib|
options[:classname] = lib
end
end.parse!
optparse.parse!
muser = ManageUser.new()
if (options.key?(:verbose)) then
muser.verbose = options[:verbose]
end
if (options.key?(:dryrun)) then
muser.dryrun = options[:dryrun]
end
if (options.key?(:gitcommit)) then
muser.git_commit = options[:gitcommit]
end
if (options.key?(:classname)) then
classname = options[:classname]
muser.classname = classname
muser.progress("overriding classname with #{classname}")
end
action = ARGV.shift
sunetid = ARGV.shift