# Copyright 2020 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. # # # module JSS # Module Constants ##################################### # Module Variables ##################################### # Module Methods ##################################### # Classes ##################################### # A Script in the JSS. # # As of Casper 9.4, the script contents as stored in the database are # accessible via the API # # According to Jamf as of early 2021, it has been some years now since # its been possible to store script contents on a dist. point - they # are all always in the database. # # Use the {#run} method to run the script on the local machine. # # @see JSS::APIObject # class Script < JSS::APIObject # Mix-Ins ##################################### include JSS::Creatable include JSS::Updatable include JSS::Categorizable # Class Methods ##################################### # Class Constants ##################################### # The base for REST resources of this class RSRC_BASE = 'scripts'.freeze # the hash key used for the JSON list output of all objects in the JSS RSRC_LIST_KEY = :scripts # The hash key used for the JSON object output. # It's also used in various error messages RSRC_OBJECT_KEY = :script # The script storage folder on the distribution point, if used DIST_POINT_SCRIPTS_FOLDER = 'Scripts'.freeze # Priority to use for running the script in relation to other actions during imaging PRIORITIES = ['Before', 'After', 'At Reboot'].freeze # which is default? DEFAULT_PRIORITY = 'After'.freeze # The keys used in the @parameters Hash PARAMETER_KEYS = [:parameter4, :parameter5, :parameter6, :parameter7, :parameter8, :parameter9, :parameter10, :parameter11].freeze # the object type for this object in # the object history table. # See {APIObject#add_object_history_entry} OBJECT_HISTORY_OBJECT_TYPE = 91 # Where is the Category in the API JSON? CATEGORY_SUBSET = :top # How is the category stored in the API data? CATEGORY_DATA_TYPE = String # Attributes ##################################### # @return [String] the file name of the script, if stored in a dist. point attr_reader :filename # @return [Array] the OS versions this can be installed onto. For all minor versions, the format is 10.5.x attr_reader :os_requirements # @return [String] either 'Before' or 'After' or "At Reboot". attr_reader :priority # @return [String] the info field for this script attr_reader :info # @return [String] the notes field for this script attr_reader :notes # @return [Hash] descriptions of parameters 4-11. Parameters 1-3 are predefined as target drive, computer name, and username attr_reader :parameters alias parameter_labels parameters alias parameter_descriptions parameters # @return {String] the actual code for this script, if it's stored in the database. attr_reader :script_contents # @return [String] the code for this script, Base64-encoded attr_reader :script_contents_encoded # Constructor ##################################### # def initialize(args = {}) super @filename = @init_data[:filename] || @name @info = @init_data[:info] @notes = @init_data[:notes] @os_requirements = @init_data[:os_requirements] ? JSS.to_s_and_a(@init_data[:os_requirements])[:arrayform] : [] @parameters = @init_data[:parameters] ? @init_data[:parameters] : {} @priority = @init_data[:priority] || DEFAULT_PRIORITY @script_contents = @init_data[:script_contents] @script_contents_encoded = @init_data[:script_contents_encoded] if @script_contents && @script_contents_encoded.to_s.empty? @script_contents_encoded = Base64.encode64 @script_contents end end # initialize # Change the script filename # # Setting it to nil will make it match the script name # # @param new_val[String,Nil] the new filename # # @return [void] # # @note This method does NOT change the filename on the distribution point # if that's where you store your scripts. # def filename=(new_val) new_val = nil if new_val == '' new_val = @name unless new_val return nil if new_val == @filename @filename = new_val @need_to_update = true end # filename= # Change the script's display name # # If the filename is the same as the name, the filename will be changed also # # @param new_val[String] the new display name # # @return [void] # def name=(new_val) new_val = new_val.to_s return if new_val == @name raise JSS::MissingDataError, "Name can't be empty" if new_val.empty? raise JSS::AlreadyExistsError, "A script already exists with the name '#{new_val}'" if JSS::Script.all_names.include? new_val # if the filename matches the name, change that too. @filename = new_val if @filename == @name @name = new_val # if our REST resource is based on the name, update that too @rest_rsrc = "#{RSRC_BASE}/name/#{CGI.escape @name.to_s}" if @rest_rsrc.include? '/name/' @need_to_update = true end # name= # Change the os_requirements # # Minumum OS's can be specified as a string using the notation ">=10.6.7" # See the {JSS.expand_min_os} method for details. # # @param new_val[String, Array] the new os requirements as a comma-separted String or an Array of Strings # # @return [void] # # @example String value # myscript.os_requirements "10.5, 10.5.3, 10.6.x" # # @example Array value # ok_oses = ['10.5', '10.5.3', '10.6.x'] # myscript.os_requirements ok_oses # # @example Minimum OS # myscript.os_requirements ">=10.7.5" # def os_requirements=(new_val) # nil should be an empty array new_val = [] if new_val.to_s.empty? # if any value starts with >=, expand it case new_val when String new_val = JSS.expand_min_os(new_val) if new_val =~ /^>=/ when Array new_val.map! { |a| a =~ /^>=/ ? JSS.expand_min_os(a) : a } new_val.flatten! new_val.uniq! else raise JSS::InvalidDataError, 'os_requirements must be a String or an Array of strings' end # case # get the array version @os_requirements = JSS.to_s_and_a(new_val)[:arrayform] @need_to_update = true end # os_requirements= # Change the priority of this script # # @param new_val[Integer] the new priority, which must be one of {PRIORITIES} # # @return [void] # def priority=(new_val) return nil if new_val == @priority new_val = DEFAULT_PRIORITY if new_val.nil? || (new_val == '') raise JSS::InvalidDataError, ":priority must be one of: #{PRIORITIES.join ', '}" unless PRIORITIES.include? new_val @priority = new_val @need_to_update = true end # priority= # Change the info field # # @param new_val[String] the new info # # @return [void] # def info=(new_val) return nil if new_val == @info # line breaks should be \r new_val = new_val.to_s.tr("\n", "\r") @info = new_val @need_to_update = true end # info= # Change the notes field # # @param new_val[String] the new notes # # @return [void] # def notes=(new_val) return nil if new_val == @notes # line breaks should be \r new_val = new_val.to_s.tr("\n", "\r") @notes = new_val @need_to_update = true end # notes= # Replace all the script parameters at once. # # This will replace the entire set with the hash provided. # # @param new_val[Hash] the Hash keys must exist in {PARAMETER_KEYS} # # @return [void] # def parameters=(new_val) return nil if new_val == @parameters new_val = {} if new_val.nil? || (new_val == '') # check the values raise JSS::InvalidDataError, ':parameters must be a Hash with keys :parameter4 thru :parameter11' unless \ new_val.is_a?(Hash) && ((new_val.keys & PARAMETER_KEYS) == new_val.keys) new_val.each do |_k, v| raise JSS::InvalidDataError, ':parameter values must be strings or nil' unless v.nil? || v.is_a?(String) end @parameters = new_val @need_to_update = true end # parameters= # Change one of the stored parameters # # @param param_num[Integer] which param are we setting? must be 4..11 # # @param new_val[String] the new value for the parameter # # @return [void] # def set_parameter(param_num, new_val) raise JSS::NoSuchItemError, 'Parameter numbers must be from 4-11' unless (4..11).cover? param_num pkey = "parameter#{param_num}".to_sym raise JSS::InvalidDataError, 'parameter values must be strings or nil' unless new_val.nil? || new_val.is_a?(String) return nil if new_val == @parameters[pkey] @parameters[pkey] = new_val @need_to_update = true end alias set_parameter_label set_parameter alias set_parameter_description set_parameter # Change the executable code of this script. # # If the arg is a Pathname instance, or a String starting with "/" # Then the arg is assumed to be a file from which to read the code. # # Otherwise it should be a String with the code itself, and it must start with '#!" # # After doing this, use {#create} or {#update} to write it to the database or # use {#upload_master_file} to save it to the master dist. point. # # @param new_val[String,Pathname] the new script contents or a path to a file containing it. # # @return [void] # def script_contents=(new_val) new_code = case new_val when String if new_val.start_with? '/' Pathname.new(new_val).read else new_val end # if when Pathname new_val.read else raise JSS::InvalidDataError, 'New code must be a String (path or code) or Pathname instance' end # case raise JSS::InvalidDataError, "Script contents must start with '#!'" unless new_code.start_with? '#!' @script_contents = new_code @script_contents_encoded = Base64.encode64 @script_contents @need_to_update = true end # Run this script on the current machine. # # If the script code is available in the {#script_contents} attribute, then that # code is saved to a tmp file, and executed. The tmp file is deleted immediately # after running # # After the script runs, this method returns a two-item Array. # - the first item is an Integer, the exit status of the script itself (0 means success) # - the second item is a String, the output (stdout + stderr) of the script. # # The exit status of the jamf binary process will be available as a Process::Status object # in $? immediately after running. # # @param opts[Hash] the options for running the script # # @option opts :target[String,Pathname] the 'target drive', passed to the script as the first commandline option. # Defaults to '/' # # @option opts :computer_name[String] the name of the computer, passed to the script as the second commandline # option. Defaults to the name of the current machine # # @option opts :username[String] the username to be passed to the script as the third commandline option. # Defaults to the current console user. # # @option opts :p4..:p11[String] the values to be passed as the 4th - 11th commandline params # Script params 1, 2, & 3 are the target:, computer_name: and username: params # # @option opts :show_output[Boolean] should the output (stdout + stderr) be copied to # stdout in realtime, as well as returned? # # @return [Array<(Integer,String)>] the exit status and stdout+stderr of the script # def run(opts = {}) raise JSS::MissingDataError, 'script_contents does not start with #!' unless @script_contents.to_s.start_with? '#!' opts[:target] ||= '/' opts[:computer_name] ||= JSS::Client.run_jamf('getComputerName')[/>(.)