### 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. ### ### ### module JSS ##################################### ### Module Methods ##################################### ### ### A Distribution Point in the JSS ### ### As well as the normal Class and Instance methods for {APIObject} subclasses, the ### DistributionPoint class provides more interaction with other parts of the API. ### ### Beyond the standard listing methods DistributionPoint.all, .all_ids, etc, every JSS ### has a single "master" distribution point. The Class method {DistributionPoint.master_distribution_point} will ### return the JSS::DistributionPoint object for that master. ### ### Also, some network segments have specific DistributionPoints assigned to them. Calling the Class method ### {DistributionPoint.my_distribution_point} will return a JSS::DistributionPoint object for your local IP address. ### ### Once you have an instance of JSS::DistributionPoint, you can mount it (on a Mac) by calling its {#mount} method ### and unmount it with {#unmount}. The {JSS::Package} and possibly {JSS::Script} classes use this to upload ### items to the master. ### ### @see JSS::APIObject ### class DistributionPoint < JSS::APIObject ##################################### ### Mix-Ins ##################################### ##################################### ### Class Constants ##################################### ### The base for REST resources of this class RSRC_BASE = "distributionpoints" ### the hash key used for the JSON list output of all objects in the JSS ### its also used in various error messages RSRC_LIST_KEY = :distribution_points ### The hash key used for the JSON object output. ### It's also used in various error messages RSRC_OBJECT_KEY = :distribution_point ### these keys, as well as :id and :name, are present in valid API JSON data for this class VALID_DATA_KEYS = [:read_only_username, :ssh_username, :is_master ] ### what are the mount options? these are comma-separated, and are passed with -o MOUNT_OPTIONS = 'nobrowse' ### An empty SHA256 digest EMPTY_PW_256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ### Set default local mount for distribution point DEFAULT_MOUNTPOINT_DIR = Pathname.new "/tmp" DEFAULT_MOUNTPOINT_PREFIX = "CasperDistribution-id" # the object type for this object in # the object history table. # See {APIObject#add_object_history_entry} OBJECT_HISTORY_OBJECT_TYPE = 76 ### Class Methods ##################################### ### Get the DistributionPoint instance for the master ### distribution point in the JSS. If there's only one ### in the JSS, return it even if not marked as master. ### ### @param refresh[Boolean] should the distribution point be re-queried? ### ### @param api[JSS::APIConnection] which API connection should we query? ### ### @return [JSS::DistributionPoint] ### def self.master_distribution_point(refresh = false, api: JSS.api) api.master_distribution_point refresh end ### Get the DistributionPoint instance for the machine running ### this code, based on its IP address. If none is defined for this IP address, ### use the result of master_distribution_point ### ### @param refresh[Boolean] should the distribution point be re-queried? ### ### @param api[JSS::APIConnection] which API connection should we query? ### ### @return [JSS::DistributionPoint] ### def self.my_distribution_point(refresh = false, api: JSS.api) api.my_distribution_point refresh end ##################################### ### Class Attributes ##################################### ### @return [String] the hostname of this DP attr_reader :ip_address ### @return [String] the local path on the server to the distribution point directory attr_reader :local_path ### @return [String] load balanacing enabled? attr_reader :enable_load_balancing ### @return [Integer] the id of the DP to use for failover attr_reader :failover_point ### @return [Boolean] is this the master DP? attr_reader :is_master ### FileService Access ### @return [String] Protocol for fileservice access (e.g. AFP, SMB) attr_reader :connection_type ### @return [Integer] the port for fileservice access attr_reader :share_port ### @return [String] the name of the fileservice sharepoint attr_reader :share_name ### @return [String] the read-write username for fileservice access attr_reader :read_write_username ### @return [String] the read-write password as a SHA256 digest attr_reader :read_write_password_sha256 ### @return [String] read-only username for fileservice attr_reader :read_only_username ### @return [String] read-only password as a SHA256 digest attr_reader :read_only_password_sha256 ### @return [String] work group or domain for SMB attr_reader :workgroup_or_domain ### http(s) access ### @return [Boolean] are http downloads available from this DP? attr_reader :http_downloads_enabled ### @return [String] the protocol to use for http downloads (http/https) attr_reader :protocol ### @return [Integer] the port for http access attr_reader :port ### @return [String] the "context" for http downloads (what goes after the hostname part of the URL) attr_reader :context ### @return [Boolean] do http downloads work without auth? attr_reader :no_authentication_required ### @return [Boolean] do http downloads use cert. authentication? attr_reader :certificate_required ### @return [Boolean] do http downloads use user/pw auth? attr_reader :username_password_required ### @return [String] the username to use for http downloads if needed for user/pw auth attr_reader :http_username ### @return [String] the password for http downloads, if needed, as a SHA256 digest attr_reader :http_password_sha256 ### @return [String] the name of the cert. used for http cert. auth. attr_reader :certificate ### @return [String] the URL for http downloads attr_reader :http_url ### @return [String] the URL to use if this one doesn't work attr_reader :failover_point_url ### ssh (scp, rsync, sftp) access ### @return [String] ssh username attr_reader :ssh_username ### @return [String] the ssh password as a SHA256 digest attr_reader :ssh_password_sha256 ### As well as the standard :id, :name, and :data, you can ### instantiate this class with :id => :master, in which case you'll ### get the Master Distribution Point as defined in the JSS. ### An error will be raised if one hasn't been defined. ### ### You can also do this more easily by calling JSS.master_distribution_point ### def initialize(args = {}) args[:api] ||= JSS.api @api = args[:api] @init_data = nil ### looking for master? if args[:id] == :master self.class.all_ids(api: @api).each do |id| @init_data = @api.get_rsrc("#{RSRC_BASE}/id/#{id}")[RSRC_OBJECT_KEY] if @init_data[:is_master] @id = @init_data[:id] @name = @init_data[:name] break end # if data is master @init_data = nil end # each id end # if args is master if @init_data.nil? super(args) end @ip_address = @init_data[:ip_address] @local_path = @init_data[:local_path] @enable_load_balancing = @init_data[:enable_load_balancing] @failover_point = @init_data[:failover_point] @is_master = @init_data[:is_master] @connection_type = @init_data[:connection_type] @share_port = @init_data[:share_port] @share_name = @init_data[:share_name] @workgroup_or_domain = @init_data[:workgroup_or_domain] @read_write_username = @init_data[:read_write_username] @read_write_password_sha256 = @init_data[:read_write_password_sha256] @read_only_username = @init_data[:read_only_username] @read_only_password_sha256 = @init_data[:read_only_password_sha256] @ssh_username = @init_data[:ssh_username] @ssh_password_sha256 = @init_data[:ssh_password_sha256] @http_username = @init_data[:http_username] @http_password_sha256 = @init_data[:http_password_sha256] @http_downloads_enabled = @init_data[:http_downloads_enabled] @protocol = @init_data[:protocol] @port = @init_data[:port] @context = @init_data[:context] @no_authentication_required = @init_data[:no_authentication_required] @certificate_required = @init_data[:certificate_required] @username_password_required = @init_data[:username_password_required] @certificate = @init_data[:certificate] @http_url = @init_data[:http_url] @failover_point_url = @init_data[:failover_point_url] @port = @init_data[:ssh_password] ### Note, as of Casper 9.3: ### :management_password_md5=>"xxxxx" ### and ### :management_password_sha256=> "xxxxxxxxxx" ### Are the read/write password ### ### An empty passwd is ### MD5 = d41d8cd98f00b204e9800998ecf8427e ### SHA256 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ### ### Seemms the read-only pw isn't available in the API ### if we mount for fileservice, where's the mountpoint? @mountpoint = Pathname.new "/#{DEFAULT_MOUNTPOINT_DIR}/#{DEFAULT_MOUNTPOINT_PREFIX}#{@id}" end #init ### ### Check the validity of a password. ### ### @param user[Symbol] one of :ro, :rw, :ssh, :http ### ### @param pw[String] the password to check for the given user ### ### @return [Boolean,Nil] was the password correct? ### nil is returned if there is no password set in the JSS. ### def check_pw(user, pw) raise JSS::InvalidDataError, "The first parameter must be one of :ro, :rw, :ssh, :http" unless [:ro, :rw, :ssh, :http].include? user sha256 = case user when :rw then @read_write_password_sha256 when :ro then @read_only_password_sha256 when :http then @http_password_sha256 when :ssh then @ssh_password_sha256 end # case return nil if sha256 == EMPTY_PW_256 sha256 == Digest::SHA2.new(256).update(pw).to_s end ### Check to see if this dist point is reachable for downloads (read-only) ### via either http, if available, or filesharing. ### ### @param pw[String] the read-only password to use for checking the connection ### If http downloads are enabled, and no http password is required ### this can be omitted. ### ### @param check_http[Boolean] should we try the http download first, if enabled? ### If you're intentionally using the ro password for filesharing, and want to check ### only filesharing, then set this to false. ### ### @return [FalseClass, Symbol] false if not reachable, otherwise :http or :mountable ### def reachable_for_download? (pw = '', check_http = true) pw ||= '' http_checked = "" if check_http && http_downloads_enabled if @username_password_required # we don't check the pw here, because if the connection fails, we'll # drop down below to try the password for mounting. # we'll escape all the chars that aren't unreserved #reserved_chars = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}]") user_pass = "#{CGI.escape @http_username}:#{CGI.escape ro_pw}@" url = @http_url.sub "://#{@ip_address}", "://#{user_pass}#{@ip_address}" else url = @http_url end begin open(url).read return :http rescue http_checked = "http and " end end # if check_http && http_downloads_enabled return :mountable if mounted? return false unless check_pw :ro , pw begin mount pw, :ro return :mountable rescue return false ensure unmount end end ### Check to see if this dist point is reachable for uploads (read-write) ### via filesharing. ### ### @param pw[String] the read-write password to use for checking the connection ### ### @return [FalseClass, Symbol] false if not reachable, otherwise :mountable ### def reachable_for_upload? (pw) return :mountable if mounted? return false unless check_pw :rw , pw begin mount pw, :rw return :mountable rescue return false ensure unmount end end ### ### Mount this distribution point locally. ### ### @param pw[String,Symbol] the read-only or read-write password for this DistributionPoint ### If :prompt, the user is promted on the commandline to enter the password for the :user. ### If :stdin#, the password is read from a line of std in represented by the digits at #, ### so :stdin3 reads the passwd from the third line of standard input. defaults to line 2, ### if no digit is supplied. see {JSS.stdin} ### ### @param access[Symbol] how to mount the DistributionPoint, and which password to expect. ### :ro (or anything else) = read-only, :rw = read-write ### ### @return [Pathname] the mountpoint. ### def mount(pw = nil, access = :ro) return @mountpoint if mounted? access = :ro unless access == :rw password = if pw == :prompt JSS.prompt_for_password "Enter the password for the #{access} user '#{access == :ro ? @read_only_username : @read_write_username }':" elsif pw.is_a?(Symbol) and pw.to_s.start_with?('stdin') pw.to_s =~ /^stdin(\d+)$/ line = $1 line ||= 2 JSS.stdin line else pw end pwok = check_pw(access, password) unless pwok msg = pwok.nil? ? "No #{access} password set in the JSS" : "Incorrect password for #{access} account" raise JSS::InvalidDataError, msg end username = access == :ro ? @read_only_username : @read_write_username safe_pw = CGI.escape password @mount_url = "#{@connection_type.downcase}://#{username}:#{safe_pw}@#{@ip_address}/#{@share_name}" @mnt_cmd = case @connection_type.downcase when 'smb' then '/sbin/mount_smbfs' when 'afp' then '/sbin/mount_afp' else raise "Can't mount distribution point #{@name}: no known connection type." end @mountpoint.mkpath mount_out = `#{@mnt_cmd} -o '#{MOUNT_OPTIONS}' '#{@mount_url}' '#{@mountpoint}' 2>&1` if $?.exitstatus == 0 and @mountpoint.mountpoint? #if system @mnt_cmd.to_s, *['-o', MOUNT_OPTIONS, @mount_url, @mountpoint.to_s] @mounted = access else @mountpoint.rmdir if @mountpoint.directory? @mounted = nil raise JSS::FileServiceError, "Can't mount #{@ip_address}: #{mount_out}" end return @mountpoint end # mount ### ### Unmount the distribution point. ### ### Does nothing if it wasn't mounted with #mount. ### ### @return [void] ### def unmount return nil unless mounted? if system "/sbin/umount '#{@mountpoint}'" sleep 1 # the umount takes time. @mountpoint.rmdir if @mountpoint.directory? and (not @mountpoint.mountpoint?) @mounted = false else raise JSS::FileServiceError ,"There was a problem unmounting #{@mountpoint}" end nil end # unmount ### ### Is this thing mounted right now? ### ### @return [Boolean] ### def mounted? @mountpoint.directory? and @mountpoint.mountpoint? end #### aliases alias hostname ip_address alias umount unmount ###################################### ### Private Instance Methods ###################################### private ### ### Unused - until I get around to making DP's updatable ### ### the XML representation of the current state of this object, ### for POSTing or PUTting back to the JSS via the API ### Will be supported for Dist Points some day, I'm sure. ### def rest_xml doc = REXML::Document.new dp = doc.add_element "distribution_point" dp.add_element(:name.to_s).text = @name dp.add_element(:ip_address.to_s).text = @ip_address dp.add_element(:local_path.to_s).text = @local_path dp.add_element(:enable_load_balancing.to_s).text = @enable_load_balancing dp.add_element(:failover_point.to_s).text = @failover_point dp.add_element(:is_master.to_s).text = @is_master dp.add_element(:connection_type.to_s).text = @connection_type dp.add_element(:share_port.to_s).text = @share_port dp.add_element(:share_name.to_s).text = @share_name dp.add_element(:read_write_username.to_s).text = @read_write_username dp.add_element(:read_write_password.to_s).text = @read_write_password dp.add_element(:read_only_username.to_s).text = @read_only_username dp.add_element(:read_only_password.to_s).text = @read_only_password dp.add_element(:workgroup_or_domain.to_s).text = @workgroup_or_domain dp.add_element(:http_downloads_enabled.to_s).text = @http_downloads_enabled dp.add_element(:protocol.to_s).text = @protocol dp.add_element(:port.to_s).text = @port dp.add_element(:context.to_s).text = @context dp.add_element(:no_authentication_required.to_s).text = @no_authentication_required dp.add_element(:certificate_required.to_s).text = @certificate_required dp.add_element(:username_password_required.to_s).text = @username_password_required dp.add_element(:http_username.to_s).text = @http_username dp.add_element(:certificate.to_s).text = @certificate dp.add_element(:http_url.to_s).text = @http_url dp.add_element(:failover_point_url.to_s).text = @failover_point_url dp.add_element(:ssh_username.to_s).text = @ssh_username dp.add_element(:ssh_password.to_s).text = @ssh_password if @ssh_password return doc.to_s end #rest_xml end # class end # module