Skip to content
Chris Lasell edited this page Nov 18, 2017 · 2 revisions
#!/usr/bin/ruby

# 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.new :id => :new, :name => @options.group, :type => :static
      else
        @group = JSS::ComputerGroup.new :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

Copyright 2017 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.
Clone this wiki locally