From a46c35e6450f59010759eda3d6324c2fe624e6db Mon Sep 17 00:00:00 2001 From: Adam Lewenberg <adamhl@stanford.edu> Date: Fri, 4 Sep 2020 17:04:10 -0700 Subject: [PATCH] Merge changes from adamhl's refactoring of tools scripts --- README.md | 87 +++++++++++- add_user | 3 + add_user.rb | 111 --------------- del_user | 3 + del_user.rb | 70 ---------- list_users | 3 + manage-user | 365 ++++++++++++++++++++++++++++++++++++++++++++++++++ refresh_users | 3 + 8 files changed, 460 insertions(+), 185 deletions(-) create mode 100644 add_user delete mode 100755 add_user.rb create mode 100644 del_user delete mode 100755 del_user.rb create mode 100755 list_users create mode 100755 manage-user create mode 100755 refresh_users diff --git a/README.md b/README.md index 80c7d0c..fc67b65 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ +[[_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" + ... +``` diff --git a/add_user b/add_user new file mode 100644 index 0000000..10829da --- /dev/null +++ b/add_user @@ -0,0 +1,3 @@ +#!/bin/bash + +./manage-user add $@ diff --git a/add_user.rb b/add_user.rb deleted file mode 100755 index 01d44b0..0000000 --- a/add_user.rb +++ /dev/null @@ -1,111 +0,0 @@ -#!/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 - diff --git a/del_user b/del_user new file mode 100644 index 0000000..ca9eb1e --- /dev/null +++ b/del_user @@ -0,0 +1,3 @@ +#!/bin/bash + +./manage-user delete $@ diff --git a/del_user.rb b/del_user.rb deleted file mode 100755 index 5ff1fa3..0000000 --- a/del_user.rb +++ /dev/null @@ -1,70 +0,0 @@ -#!/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 - diff --git a/list_users b/list_users new file mode 100755 index 0000000..fdb188e --- /dev/null +++ b/list_users @@ -0,0 +1,3 @@ +#!/bin/bash + +./manage-user list $@ diff --git a/manage-user b/manage-user new file mode 100755 index 0000000..9b9b8ef --- /dev/null +++ b/manage-user @@ -0,0 +1,365 @@ +#!/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 +muser.progress("action: #{action}") +muser.progress("sunetid: #{sunetid}") +muser.progress("classname: #{classname}") +muser.progress("dryrun: #{muser.dryrun}") +muser.progress("gitcommit: #{muser.git_commit}") + +if ((action =~ /^(add|delete)$/) && (! sunetid)) then + exit_with_error("You must provide a sunetid with the #{action} action.") +end + +if (action !~ /^(add|delete|refresh|list)$/) then + exit_with_error("The action must be one of 'add', 'delete', 'list', or 'refresh'") +end + +## # #### # #### # #### # #### # #### # #### # #### # #### # #### # #### +### Read in the existing users from the YAML file. + +# Set the data source +muser.set_data_source() +muser.progress(muser.hiera_data) + +## # #### # #### # #### # #### # #### # #### # #### # #### # #### # #### +### Take action. +if (action == 'add') then + muser.add_user(sunetid) +elsif (action == 'delete') then + muser.delete_user(sunetid) +elsif (action == 'refresh') then + muser.refresh_data() +elsif (action == 'list') then + muser.list_data() +end diff --git a/refresh_users b/refresh_users new file mode 100755 index 0000000..0ad1afd --- /dev/null +++ b/refresh_users @@ -0,0 +1,3 @@ +#!/bin/bash + +./manage-user refresh $@ -- GitLab