#!/usr/bin/env ruby ### Copyright 2022 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 = %i[create_group rename_group delete_group add_members remove_members remove_all list_members] ACTIONS_FOR_STATIC_GROUPS_ONLY = %i[create_group add_members remove_members remove_all] ##################################### ### Attributes attr_reader :debug ##################################### ### ### set up ### def initialize @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 Jamf.cnx.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 @group = if @options.action == :create_group JSS::ComputerGroup.make name: @options.group, type: :static else JSS::ComputerGroup.fetch name: @options.group end end # if ACTIONS_NEEDING_GROUP # smart groups can't have some things done to them if ACTIONS_FOR_STATIC_GROUPS_ONLY.include? @options.action and @group.smart? raise InvalidTypeError, "You can't do that to a smart group. Use the JSS WebApp if needed." end 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/ruby-jss.conf and ~/.ruby-jss.conf. See the ruby-jss 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 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 add_members unless @options.computers.empty? 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| @group.add_member c rescue JSS::NoSuchItemError puts "#{$!} - skipping" # 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| @group.remove_member c rescue JSS::NoSuchItemError puts "#{$!} - skipping" 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 false end # confirm end # class App ####################################### begin app = App.new app.run rescue # handle exceptions not handled elsewhere puts "An error occurred: #{$!}" puts 'Backtrace:' if app.debug puts $@ if app.debug end