#!/usr/bin/ruby ### Copyright 2018 Pixar ### ### Licensed under the Apache License, Version 2.0 (the "Apache License") ### with the following modification; you may not use this file except in ### compliance with the Apache License and the following modification to it: ### Section 6. Trademarks. is deleted and replaced with: ### ### 6. Trademarks. This License does not grant permission to use the trade ### names, trademarks, service marks, or product names of the Licensor ### and its affiliates, except as required to comply with Section 4(c) of ### the License and to reproduce the content of the NOTICE file. ### ### You may obtain a copy of the Apache License at ### ### http://www.apache.org/licenses/LICENSE-2.0 ### ### Unless required by applicable law or agreed to in writing, software ### distributed under the Apache License with the above modification is ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ### KIND, either express or implied. See the Apache License for the specific ### language governing permissions and limitations under the Apache License. # Create or change the membership of a computer group in the JSS # Load in the JSS library require 'jss-api' # Load other libs require 'getoptlong' require 'ostruct' class App ##################################### ### ### Constants ### USAGE = "Usage: #{File.basename($0)} [-LsmcdlarRC] [--help] [-n newname] [-S server] [-U user] [-T timeout] [-V] [--debug] group [-f /file/path ] [computer [computer ...]]" ACTIONS_NEEDING_GROUP = [ :create_group, :rename_group, :delete_group, :add_members, :remove_members, :remove_all, :list_members] ACTIONS_FOR_STATIC_GROUPS_ONLY = [:create_group, :add_members, :remove_members, :remove_all] ##################################### ### Attributes attr_reader :debug ##################################### ### ### set up ### def initialize(args) @debug = false # define the options cli_opts = GetoptLong.new( [ '--help', '-h', '-H', GetoptLong::NO_ARGUMENT ], [ '--list-groups', '-L', GetoptLong::NO_ARGUMENT ], [ '--list-static', '-s', GetoptLong::NO_ARGUMENT ], [ '--list-smart', '-m', GetoptLong::NO_ARGUMENT ], [ '--create-group', '--create', '-c', GetoptLong::NO_ARGUMENT ], [ '--rename-group', '--rename', '-n', GetoptLong::REQUIRED_ARGUMENT ], [ '--delete-group', '--delete', '-d', GetoptLong::NO_ARGUMENT ], [ '--list-members', '--list-computers', '-l', GetoptLong::NO_ARGUMENT ], [ '--add-members', '--add', '-a', GetoptLong::NO_ARGUMENT ], [ '--remove-members', '--remove', '-r', GetoptLong::NO_ARGUMENT ], [ '--remove-all-members', '-R', GetoptLong::NO_ARGUMENT ], [ '--file', '-f', GetoptLong::REQUIRED_ARGUMENT ], [ '--server', '-S', GetoptLong::OPTIONAL_ARGUMENT], [ '--port', '-P', GetoptLong::OPTIONAL_ARGUMENT], [ '--user', '-U', GetoptLong::OPTIONAL_ARGUMENT], [ '--no-verify-cert', '-V', GetoptLong::NO_ARGUMENT], [ '--timeout', '-T', GetoptLong::OPTIONAL_ARGUMENT], [ '--no-confirm', '-C', GetoptLong::NO_ARGUMENT], [ '--debug', GetoptLong::NO_ARGUMENT] ) # here's where we hold cmdline args and other user options @options = OpenStruct.new # set defaults @options.action = :none # if stdin is not a tty, then we must assume # we're being passed a password @options.getpass = $stdin.tty? ? :prompt : :stdin # parse the options cli_opts.each do |opt, arg| case opt when '--help' show_help when '--list-groups' @options.action = :list_groups when '--list-static' @options.action = :list_static when '--list-smart' @options.action = :list_smart when '--list-members' @options.action = :list_members when '--create-group' @options.action = :create_group when '--rename-group' @options.action = :rename_group @options.new_name = arg when '--delete-group' @options.action = :delete_group when '--add-members' @options.action = :add_members when '--remove-members' @options.action = :remove_members when '--remove-all-members' @options.action = :remove_all when '--file' @options.input_file = Pathname.new arg when '--server' @options.server = arg when '--port' @options.port = arg when '--user' @options.user = arg when '--no-verify-cert' @options.verify_cert = false when '--timeout' @options.timeout = arg when '--no-confirm' @options.no_confirm = true when '--debug' @debug = true end # case end # opts.each @options.group = ARGV.shift # if we were given a file of computer names, read it in @options.computers = @options.input_file ? get_computers_from_file : [] # and add any computers on the commandline @options.computers += ARGV # will we say anything when finished? @done_msg = nil end # init ##################################### ### ### Do It ### def run if @options.action == :none puts USAGE return end # use any config settings defined.... @options.user ||= JSS::CONFIG.api_username @options.server ||= JSS::CONFIG.api_server_name raise JSS::MissingDataError, "No JSS Username provided or found in the JSS gem config." unless @options.user raise JSS::MissingDataError, "No JSS Server provided or found in the JSS gem config." unless @options.server JSS.api.connect( :server => @options.server, :port => @options.port, :verify_cert => @options.verify_cert, :user => @options.user, :pw => @options.getpass, :stdin_line => 1, :timeout => @options.timeout ) if ACTIONS_NEEDING_GROUP.include? @options.action raise JSS::MissingDataError, "Please specify a group name" unless @options.group # get the group from the API if @options.action == :create_group @group = JSS::ComputerGroup.make :name => @options.group, :type => :static else @group = JSS::ComputerGroup.fetch :name => @options.group end end # if ACTIONS_NEEDING_GROUP # smart groups can't have some things done to them raise InvalidTypeError, "You can't do that to a smart group. Use the JSS WebApp if needed." if ACTIONS_FOR_STATIC_GROUPS_ONLY.include? @options.action and @group.smart? case @options.action when :list_groups list_groups when :list_static list_groups :static when :list_smart list_groups :smart when :list_members list_members when :create_group create_group when :rename_group rename_group when :delete_group delete_group when :add_members add_members when :remove_members remove_members when :remove_all remove_all end # case @options.action puts "Done! #{@done_msg}" if @done_msg end # run ##################################### ### ### Show Help ### def show_help puts <<-FULLHELP A tool for working with computer groups in the JSS. #{USAGE} Options: -L, --list-groups - list all computer groups in the JSS -s, --list-static - list all static computer groups in the JSS -m, --list-smart - list all smart computer groups in the JSS -c, --create-group - create a new static computer group in the JSS -n, --rename newname - rename the specified computer group to newname -d, --delete - delete the specified computer group (static groups only) -l, --list-members - list all the computers in the group specified -a, --add-members - add the specified computer(s) to the specified group -r, --remove-members - remove the specified computer(s) from the specified group -R, --remove-all - remove all computers from the specified group -f, --file /path/... - read computer names/ids from the file at /path/... -S, --server srvr - specify the JSS API server name -P, --port portnum - specify the JSS API port -U, --user username - specify the JSS API user -V, --no-verify-cert - Allow self-signed, unverified SSL certificate -T, --timeout secs - specify the JSS API timeout -C - don't ask for confirmation before acting --debug - show the ruby backtrace when errors occur -H, --help - show this help Notes: - If no API settings are provided, they will be read from /etc/jss_gem.conf and ~/.jss_gem.conf. See the JSS Gem docs for details. - The password for the connection will be read from STDIN or prompted if needed - Computers can be specified by name or JSS id number. If a name exists more than once in the JSS, the machine is skipped. Use IDs to avoid this. - Only static groups can be modified. Use the JSS WebUI for editing smart groups - If a file is used to specify computers, they are combined with any specified on the commandline. - Files of computers must be whitespace-separated (spaces, tabs, & returns in any number or combination) FULLHELP return end ##################################### ### ### Spit out a list of all computer groups ### def list_groups(show = :all) case show when :all label = "All" groups_to_show = JSS::ComputerGroup.all when :static label = "Static" groups_to_show = JSS::ComputerGroup.all_static when :smart label = "Smart" groups_to_show = JSS::ComputerGroup.all_smart end #case puts "# #{label} computer groups in the JSS" puts "#---------------------------------------------" groups_to_show.sort{|a,b| a[:name].downcase <=> b[:name].downcase}.each do |grp| puts grp[:name] end end ##################################### ### ### Spit out a list of all computers in a group ### def list_members puts "# All members of JSS #{@group.smart? ? 'smart' : 'static'} computer group '#{@options.group}'" puts "#--- name (id) ---------------------------------" # put them into a tmp array, so that # we can sort by computer name, remembering that # there can be duplicate names. list = [] @group.members.each{|mem| list << "#{mem[:name]} (#{mem[:id]})" } puts list.sort #.join("\n") end ##################################### ### ### Create a new group ### def create_group return unless confirm "create a new static group named '#{@options.group}'" @group.create unless @options.computers.empty? add_members end end ##################################### ### ### rename a group ### def rename_group return unless confirm "rename group '#{@group.name}' to '#{@options.new_name}'" @group.name = @options.new_name @group.update end ##################################### ### ### delete a group ### def delete_group return unless confirm "DELETE group '#{@group.name}'" @group.delete end ##################################### ### ### add members to a group ### def add_members raise JSS::MissingDataError, "No computer names provided" if @options.computers.empty? raise JSS::UnsupportedError, "Smart group members can't be changed." if @group.smart? return unless @options.action == :create_group or confirm "add computers to group '#{@group.name}'" @options.computers.each do |c| begin @group.add_member c rescue JSS::NoSuchItemError puts "#{$!} - skipping" end # begin end # each @group.update end ##################################### ### ### remove members from a group ### def remove_members raise JSS::MissingDataError, "No computer names provided" if @options.computers.empty? raise JSS::UnsupportedError, "Smart group members can't be changed." if @group.smart? return unless confirm "remove computers from group '#{@group.name}'" @options.computers.each do |c| begin @group.remove_member c rescue JSS::NoSuchItemError puts "#{$!} - skipping" end end @group.update end ##################################### ### ### remove all members from a group ### def remove_all raise JSS::UnsupportedError, "Smart group members can't be changed." if @group.smart? return unless confirm "remove ALL computers from group '#{@group.name}'" @group.clear @group.update end ##################################### ### ### Read computer names from a file ### Generally the names should be one per line, but ### they can be separated by any whitespace. ### Returns an array of computer names from the file. ### def get_computers_from_file raise JSS::NoSuchItemError "File #{@options.input_file} isn't a file or isn't readable." unless \ @options.input_file.file? and @options.input_file.readable? @options.input_file.read.split(/\s+/) end ##################################### ### ### Get confirmation before doing something ### Returns true or false ### def confirm (action) return true if @options.no_confirm print "Really #{action}? (y/n): " $stdin.reopen '/dev/tty' reply = $stdin.gets.strip return true if reply =~ /^y/i return false end # confirm end # class App ####################################### begin app = App.new(ARGV) app.run rescue # handle exceptions not handled elsewhere puts "An error occurred: #{$!}" puts "Backtrace:" if app.debug puts $@ if app.debug ensure end