lib/jss/api_connection.rb in ruby-jss-1.4.1 vs lib/jss/api_connection.rb in ruby-jss-1.5.1
- old
+ new
@@ -23,22 +23,10 @@
###
###
module JSS
- # Constants
- #####################################
-
- # Module Variables
- #####################################
-
- # Module Methods
- #####################################
-
- # Classes
- #####################################
-
# Instances of this class represent a REST connection to a JSS API.
#
# For most cases, a single connection to a single JSS is all you need, and
# this is ruby-jss's default behavior.
#
@@ -269,11 +257,11 @@
# is all you'll need. However to access API resources that aren't yet
# implemented in other parts of ruby-jss, you can use the methods
# {#get_rsrc}, {#put_rsrc}, {#post_rsrc}, & {#delete_rsrc}
# documented below.
#
- # For even lower-level work, you can access the underlying RestClient::Resource
+ # For even lower-level work, you can access the underlying Faraday::Connection
# inside the APIConnection via the connection's {#cnx} attribute.
#
# APIConnection instances also have a {#server} attribute which contains an
# instance of {JSS::Server} q.v., representing the JSS to which it's connected.
#
@@ -328,18 +316,24 @@
EXTENDABLE_CLASSES = [JSS::Computer, JSS::MobileDevice, JSS::User].freeze
# values for the format param of get_rsrc
GET_FORMATS = %i[json xml].freeze
+ HTTP_ACCEPT_HEADER = 'Accept'.freeze
+ HTTP_CONTENT_TYPE_HEADER = 'Content-Type'.freeze
+
+ MIME_JSON = 'application/json'.freeze
+ MIME_XML = 'application/xml'.freeze
+
# Attributes
#####################################
# @return [String] the username who's connected to the JSS API
attr_reader :user
alias jss_user user
- # @return [RestClient::Resource] the underlying connection resource
+ # @return [Faraday::Connection] the underlying connection resource
attr_reader :cnx
# @return [Boolean] are we connected right now?
attr_reader :connected
alias connected? connected
@@ -357,11 +351,11 @@
attr_reader :port
# @return [String] the protocol being used: http or https
attr_reader :protocol
- # @return [RestClient::Response] The response from the most recent API call
+ # @return [Faraday::Response] The response from the most recent API call
attr_reader :last_http_response
# @return [String] The base URL to to the current REST API
attr_reader :rest_url
@@ -451,12 +445,10 @@
# @option args :port[Integer] the port number to connect with, defaults to 8443
#
# @option args :use_ssl[Boolean] should the connection be made over SSL? Defaults to true.
#
# @option args :verify_cert[Boolean] should HTTPS SSL certificates be verified. Defaults to true.
- # If your connection raises RestClient::SSLCertificateNotVerified, and you don't care about the
- # validity of the SSL cert. just set this explicitly to false.
#
# @option args :user[String] a JSS user who has API privs, required if not defined in JSS::CONFIG
#
# @option args :pw[String,Symbol] Required, the password for that user, or :prompt, or :stdin
# If :prompt, the user is promted on the commandline to enter the password for the :user.
@@ -492,11 +484,11 @@
# figure out :password from :pw
args[:password] = acquire_password args
# heres our connection
- @cnx = RestClient::Resource.new(@rest_url.to_s, args)
+ @cnx = create_connection args[:password]
verify_server_version
@name = "#{@user}@#{@server_host}:#{@port}" if @name.nil? || @name == :disconnected
@connected ? hostname : nil
@@ -541,12 +533,11 @@
@server_host = nil
@cnx = nil
@connected = false
end # disconnect
- # Get an arbitrary JSS resource
- #
+ # Get a JSS resource
# The first argument is the resource to get (the part of the API url
# after the 'JSSResource/' ) The resource must be properly URL escaped
# beforehand. Note: URL.encode is deprecated, use CGI.escape
#
# By default we get the data in JSON, and parse it into a ruby Hash
@@ -570,18 +561,23 @@
#
def get_rsrc(rsrc, format = :json, raw_json: false)
validate_connected
raise JSS::InvalidDataError, 'format must be :json or :xml' unless GET_FORMATS.include? format
- begin
- @last_http_response = @cnx[rsrc].get(accept: format)
- return JSON.parse(@last_http_response.body, symbolize_names: true) if format == :json && !raw_json
+ @last_http_response =
+ @cnx.get(rsrc) do |req|
+ req.headers[HTTP_ACCEPT_HEADER] = format == :json ? MIME_JSON : MIME_XML
+ end
- @last_http_response.body
- rescue RestClient::ExceptionWithResponse => e
- handle_http_error e
+ unless @last_http_response.success?
+ handle_http_error
+ return
end
+
+ return JSON.parse(@last_http_response.body, symbolize_names: true) if format == :json && !raw_json
+
+ @last_http_response.body
end
# Update an existing JSS resource
#
# @param rsrc[String] the API resource being changed, the URL part after 'JSSResource/'
@@ -595,55 +591,75 @@
# convert CRs & to
xml.gsub!(/\r/, ' ')
# send the data
- @last_http_response = @cnx[rsrc].put(xml, content_type: 'text/xml')
+ @last_http_response =
+ @cnx.put(rsrc) do |req|
+ req.headers[HTTP_CONTENT_TYPE_HEADER] = MIME_XML
+ req.headers[HTTP_ACCEPT_HEADER] = MIME_XML
+ req.body = xml
+ end
+ unless @last_http_response.success?
+ handle_http_error
+ return
+ end
+
@last_http_response.body
- rescue RestClient::ExceptionWithResponse => e
- handle_http_error e
end
# Create a new JSS resource
#
# @param rsrc[String] the API resource being created, the URL part after 'JSSResource/'
#
# @param xml[String] the xml specifying the new object.
#
# @return [String] the xml response from the server.
#
- def post_rsrc(rsrc, xml = '')
+ def post_rsrc(rsrc, xml)
validate_connected
# convert CRs & to
- xml.gsub!(/\r/, ' ') if xml
+ xml&.gsub!(/\r/, ' ')
# send the data
- @last_http_response = @cnx[rsrc].post(xml, content_type: 'text/xml', accept: :json)
+ @last_http_response =
+ @cnx.post(rsrc) do |req|
+ req.headers[HTTP_CONTENT_TYPE_HEADER] = MIME_XML
+ req.headers[HTTP_ACCEPT_HEADER] = MIME_XML
+ req.body = xml
+ end
+ unless @last_http_response.success?
+ handle_http_error
+ return
+ end
@last_http_response.body
- rescue RestClient::ExceptionWithResponse => e
- handle_http_error e
end # post_rsrc
# Delete a resource from the JSS
#
# @param rsrc[String] the resource to create, the URL part after 'JSSResource/'
#
# @return [String] the xml response from the server.
#
- def delete_rsrc(rsrc, xml = nil)
+ def delete_rsrc(rsrc)
validate_connected
raise MissingDataError, 'Missing :rsrc' if rsrc.nil?
- # payload?
- return delete_with_payload rsrc, xml if xml
-
# delete the resource
- @last_http_response = @cnx[rsrc].delete
+ @last_http_response =
+ @cnx.delete(rsrc) do |req|
+ req.headers[HTTP_CONTENT_TYPE_HEADER] = MIME_XML
+ req.headers[HTTP_ACCEPT_HEADER] = MIME_XML
+ end
+
+ unless @last_http_response.success?
+ handle_http_error
+ return
+ end
+
@last_http_response.body
- rescue RestClient::ExceptionWithResponse => e
- handle_http_error e
end # delete_rsrc
# Test that a given hostname & port is a JSS API server
#
# @param server[String] The hostname to test,
@@ -655,300 +671,29 @@
def valid_server?(server, port = SSL_PORT)
# cheating by shelling out to curl, because getting open-uri, or even net/http to use
# ssl_options like :OP_NO_SSLv2 and :OP_NO_SSLv3 will take time to figure out..
return true if `/usr/bin/curl -s 'https://#{server}:#{port}/#{TEST_PATH}'`.include? TEST_CONTENT
return true if `/usr/bin/curl -s 'http://#{server}:#{port}/#{TEST_PATH}'`.include? TEST_CONTENT
- false
- # # try ssl first
- # # NOTE: doesn't work if we can't disallow SSLv3 or force TLSv1
- # # See cheat above.
- # begin
- # return true if open("https://#{server}:#{port}/#{TEST_PATH}", ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE).read.include? TEST_CONTENT
- #
- # rescue
- # # then regular http
- # begin
- # return true if open("http://#{server}:#{port}/#{TEST_PATH}").read.include? TEST_CONTENT
- # rescue
- # # any errors = no API
- # return false
- # end # begin
- # end # begin
- # # if we're here, no API
- # false
+ false
end
# The server to which we are connected, or will
# try connecting to if none is specified with the
# call to #connect
#
# @return [String] the hostname of the server
#
def hostname
return @server_host if @server_host
+
srvr = JSS::CONFIG.api_server_name
srvr ||= JSS::Client.jss_server
srvr
end
alias host hostname
- #################
-
- # Call one of the 'all*' methods on a JSS::APIObject subclass
- # using this APIConnection.
- #
- #
- # @deprecated please use the .all class method of the desired class
- #
- # @param class_name[String,Symbol] The name of a JSS::APIObject subclass
- # see {JSS.api_object_class}
- #
- # @param refresh[Boolean] Should the data be re-read from the API?
- #
- # @param only[String,Symbol] Limit the output to subset or data. All
- # APIObject subclasses can take :ids or :names, which calls the .all_ids
- # and .all_names methods. Some subclasses can take other options, e.g.
- # MobileDevice can take :udids
- #
- # @return [Array] The list of items for the class
- #
- def all(class_name, refresh = false, only: nil)
- the_class = JSS.api_object_class(class_name)
- list_method = only ? :"all_#{only}" : :all
-
- raise ArgumentError, "Unknown identifier: #{only} for #{the_class}" unless
- the_class.respond_to? list_method
-
- the_class.send list_method, refresh, api: self
- end
-
- # Call the 'map_all_ids_to' method on a JSS::APIObject subclass
- # using this APIConnection.
- #
- # @deprecated please use the .map_all_ids_to class method of the desired class
- #
- #
- # @param class_name[String,Symbol] The name of a JSS::APIObject subclass
- # see {JSS.api_object_class}
- #
- # @param refresh[Boolean] Should the data be re-read from the API?
- #
- # @param to[String,Symbol] the value to which the ids should be mapped
- #
- # @return [Hash] The ids for the class keyed to the requested identifier
- #
- def map_all_ids(class_name, refresh = false, to: nil)
- raise "'to:' value must be provided for mapping ids." unless to
- the_class = JSS.api_object_class(class_name)
- the_class.map_all_ids_to to, refresh, api: self
- end
-
- # Call the 'valid_id' method on a JSS::APIObject subclass
- # using this APIConnection. See {JSS::APIObject.valid_id}
- #
- # @deprecated please use the .valid_id class method of the desired class
- #
- #
- # @param class_name[String,Symbol] The name of a JSS::APIObject subclass,
- # see {JSS.api_object_class}
- #
- # @param identifier[String,Symbol] the value to which the ids should be mapped
- #
- # @param refresh[Boolean] Should the data be re-read from the API?
- #
- # @return [Integer, nil] the id of the matching object of the class,
- # or nil if there isn't one
- #
- def valid_id(class_name, identifier, refresh = true)
- the_class = JSS.api_object_class(class_name)
- the_class.valid_id identifier, refresh, api: self
- end
-
- # Call the 'exist?' method on a JSS::APIObject subclass
- # using this APIConnection. See {JSS::APIObject.exist?}
- #
- # @deprecated please use the .exist class method of the desired class
- #
- # @param class_name[String,Symbol] The name of a JSS::APIObject subclass
- # see {JSS.api_object_class}
- #
- # @param identifier[String,Symbol] the value to which the ids should be mapped
- #
- # @param refresh[Boolean] Should the data be re-read from the API?
- #
- # @return [Boolean] Is there an object of this class in the JSS matching
- # this indentifier?
- #
- def exist?(class_name, identifier, refresh = false)
- !valid_id(class_name, identifier, refresh).nil?
- end
-
- # Call {Matchable.match} for the given class.
- #
- # See {Matchable.match}
- #
- # @deprecated Please use the .match class method of the desired class
- #
- # @param class_name[String,Symbol] The name of a JSS::APIObject subclass
- # see {JSS.api_object_class}
- #
- # @return (see Matchable.match)
- #
- def match(class_name, term)
- the_class = JSS.api_object_class(class_name)
- raise JSS::UnsupportedError, "Class #{the_class} is not matchable" unless the_class.respond_to? :match
- the_class.match term, api: self
- end
-
- # Retrieve an object of a given class from the API
- # See {APIObject.fetch}
- #
- # @deprecated Please use the .fetch class method of the desired class
- #
- #
- # @param class_name[String,Symbol] The name of a JSS::APIObject subclass
- # see {JSS.api_object_class}
- #
- # @return [APIObject] The ruby-instance of the object.
- #
- def fetch(class_name, arg)
- the_class = JSS.api_object_class(class_name)
- the_class.fetch arg, api: self
- end
-
- # Make a ruby instance of a not-yet-existing APIObject
- # of the given class
- # See {APIObject.make}
- #
- # @deprecated Please use the .make class method of the desired class
- #
- # @param class_name[String,Symbol] The name of a JSS::APIObject subclass
- # see {JSS.api_object_class}
- #
- # @return [APIObject] The un-created ruby-instance of the object.
- #
- def make(class_name, **args)
- the_class = JSS.api_object_class(class_name)
- args[:api] = self
- the_class.make args
- end
-
- # Call {JSS::Computer.checkin_settings} q.v., passing this API
- # connection
- # @deprecated Please use JSS::Computer.checkin_settings
- #
- def computer_checkin_settings
- JSS::Computer.checkin_settings api: self
- end
-
- # Call {JSS::Computer.inventory_collection_settings} q.v., passing this API
- # connection
- # @deprecated Please use JSS::Computer.inventory_collection_settings
- #
- def computer_inventory_collection_settings
- JSS::Computer.inventory_collection_settings api: self
- end
-
- # Call {JSS::Computer.application_usage} q.v., passing this API
- # connection
- # @deprecated Please use JSS::Computer.application_usage
- #
- def computer_application_usage(ident, start_date, end_date = nil)
- JSS::Computer.application_usage ident, start_date, end_date, api: self
- end
-
- # Call {JSS::Computer.management_data} q.v., passing this API
- # connection
- #
- # @deprecated Please use JSS::Computer.management_data
- #
- def computer_management_data(ident, subset: nil, only: nil)
- JSS::Computer.management_data ident, subset: subset, only: only, api: self
- end
-
- # Call {JSS::Computer.history} q.v., passing this API
- # connection
- #
- # @deprecated Please use JSS::Computer.management_history or its
- # convenience methods. @see JSS::ManagementHistory
- #
- def computer_history(ident, subset: nil)
- JSS::Computer.history ident, subset, api: self
- end
-
- # Call {JSS::Computer.send_mdm_command} q.v., passing this API
- # connection
- #
- # @deprecated Please use JSS::Computer.send_mdm_command or its
- # convenience methods. @see JSS::MDM
- #
- def send_computer_mdm_command(targets, command, passcode = nil)
- opts = passcode ? { passcode: passcode } : {}
- JSS::Computer.send_mdm_command targets, command, opts: opts, api: self
- end
-
- # 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] re-read from the API?
- #
- # @return [JSS::DistributionPoint]
- #
- def master_distribution_point(refresh = false)
- JSS::DistributionPoint.master_distribution_point refresh, api: self
- 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?
- #
- # @return [JSS::DistributionPoint]
- #
- def my_distribution_point(refresh = false)
- JSS::DistributionPoint.my_distribution_point refresh, api: self
- end
-
- # @deprecated
- #
- # @see {JSS::NetworkSegment.network_ranges}
- #
- def network_ranges(refresh = false)
- JSS::NetworkSegment.network_ranges refresh, api: self
- end # def network_segments
-
- # @deprecated
- #
- # @see {JSS::NetworkSegment.network_segments_for_ip}
- #
- def network_segments_for_ip(ip, refresh = false)
- JSS::NetworkSegment.network_segments_for_ip ip, refresh, api: self
- end
-
- # @deprecated
- #
- # @see {JSS::NetworkSegment.my_network_segments}
- #
- def my_network_segments
- network_segments_for_ip JSS::Client.my_ip_address
- end
-
- # Send an MDM command to one or more mobile devices managed by
- # this JSS
- #
- # see {JSS::MobileDevice.send_mdm_command}
- #
- # @deprecated Please use JSS::MobileDevice.send_mdm_command or its
- # convenience methods. @see JSS::MDM
- #
- def send_mobiledevice_mdm_command(targets, command, data = {})
- JSS::MobileDevice.send_mdm_command(targets, command, opts: data, api: self)
- end
-
# Empty all cached lists from this connection
# then run garbage collection to clear any available memory
#
# If an APIObject Subclass's RSRC_LIST_KEY is specified, only the caches
# for that class are flushed (e.g. :computers, :comptuer_groups)
@@ -1050,10 +795,11 @@
#
# @return [Hash] The args with defaults applied
#
def apply_defaults_from_client(args)
return unless JSS::Client.installed?
+
# these settings can come from the jamf binary config, if this machine is a JSS client.
args[:server] ||= JSS::Client.jss_server
args[:port] ||= JSS::Client.jss_port.to_i
args[:use_ssl] ||= JSS::Client.jss_protocol.to_s.end_with? 's'
args
@@ -1103,15 +849,17 @@
# regardless of their permissions.
# However, it's marked as 'deprecated'. Hopefully jamf will
# keep this basic level of info available for basic authentication
# and JSS version checking.
begin
- @server = JSS::Server.new get_rsrc('jssuser')[:user], self
- rescue RestClient::Unauthorized
+ data = get_rsrc('jssuser')
+ rescue JSS::AuthorizationError
raise JSS::AuthenticationError, "Incorrect JSS username or password for '#{@user}@#{@server_host}:#{@port}'."
end
+ @server = JSS::Server.new data[:user], self
+
min_vers = JSS.parse_jss_version(JSS::MINIMUM_SERVER_VERSION)[:version]
return if @server.version >= min_vers # we're good...
err_msg = "JSS version #{@server.raw_version} to low. Must be >= #{min_vers}"
@connected = false
@@ -1176,81 +924,72 @@
def verify_ssl(args)
# use SSL for SSL ports unless specifically told not to
if SSL_PORTS.include? args[:port]
args[:use_ssl] = true unless args[:use_ssl] == false
end
+ return unless args[:use_ssl]
+
# if verify_cert is anything but false, we will verify
- args[:verify_ssl] = args[:verify_cert] == false ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
+ args[:verify_ssl] = args[:verify_cert] != false
+
+ # ssl version if not specified
+ args[:ssl_version] ||= DFT_SSL_VERSION
+
+ @ssl_options = {
+ verify: args[:verify_ssl],
+ version: args[:ssl_version]
+ }
end
- # Parses the HTTP body of a RestClient::ExceptionWithResponse
- # (the parent of all HTTP error responses) and its subclasses
- # and re-raises a JSS::APIError with a more
- # useful error message.
+ # Parses the @last_http_response
+ # and raises a JSS::APIError with a useful error message.
#
- # @param exception[RestClient::ExceptionWithResponse] the exception to parse
- #
# @return [void]
#
- def handle_http_error(exception)
- @last_http_response = exception.response
- case exception
- when RestClient::ResourceNotFound
- # other methods catch this and report more details
- raise exception
- when RestClient::Conflict
+ def handle_http_error
+ return if @last_http_response.success?
+
+ case @last_http_response.status
+ when 404
+ err = JSS::NoSuchItemError
+ msg = 'Not Found'
+ when 409
err = JSS::ConflictError
- msg_matcher = /<p>Error:(.*?)(<|$)/m
- when RestClient::BadRequest
+ @last_http_response.body =~ /<p>(The server has not .*?)(<|$)/m
+ msg = Regexp.last_match(1)
+ when 400
err = JSS::BadRequestError
- msg_matcher = %r{>Bad Request</p>\n<p>(.*?)</p>\n<p>You can get technical detail}m
- when RestClient::Unauthorized
- raise
+ @last_http_response.body =~ %r{>Bad Request</p>\n<p>(.*?)</p>\n<p>You can get technical detail}m
+ msg = Regexp.last_match(1)
+ when 401
+ err = JSS::AuthorizationError
+ msg = 'You are not authorized to do that.'
+ when (500..599)
+ err = JSS::APIRequestError
+ msg = 'There was an internal server error'
else
err = JSS::APIRequestError
- msg_matcher = %r{<body.*?>(.*?)</body>}m
+ msg = "There was a error processing your request, status: #{@last_http_response.status}"
end
- exception.http_body =~ msg_matcher
- msg = Regexp.last_match(1)
- msg ||= exception.http_body
raise err, msg
end
- # RestClient::Resource#delete doesn't take an HTTP payload,
- # but some JSS API resources require it (notably, logflush).
- #
- # This method uses RestClient::Request#execute
- # to do the same thing that RestClient::Resource#delete does, but
- # adding the payload.
- #
- # @param rsrc[String] the sub-resource we're DELETEing
- #
- # @param payload[String] The XML to be passed with the DELETE
- #
- # @param additional_headers[Type] See RestClient::Request#execute
- #
- # @param &block[Type] See RestClient::Request#execute
- #
- # @return [String] the XML response from the server.
- #
- def delete_with_payload(rsrc, payload, additional_headers = {}, &block)
- headers = (@cnx.options[:headers] || {}).merge(additional_headers)
- @last_http_response = RestClient::Request.execute(
- @cnx.options.merge(
- method: :delete,
- url: @cnx[rsrc].url,
- payload: payload,
- headers: headers
- ),
- &(block || @block)
- )
- rescue RestClient::ExceptionWithResponse => e
- handle_http_error e
- end # delete_with_payload
+ # create the faraday connection object
+ def create_connection(pw)
+ Faraday.new(@rest_url, ssl: @ssl_options) do |cnx|
+ cnx.basic_auth @user, pw
+ cnx.options[:timeout] = @timeout
+ cnx.options[:open_timeout] = @open_timeout
+ cnx.adapter Faraday::Adapter::NetHttp
+ end
+ end
end # class APIConnection
+ # JSS MODULE METHODS
+ ######################
+
# Create a new APIConnection object and use it for all
# future API calls. If connection options are provided,
# they are passed to the connect method immediately, otherwise
# JSS.api.connect must be called before attemting to use the
# connection.
@@ -1274,9 +1013,10 @@
#
# @return [APIConnection] The connection now being used.
#
def self.use_api_connection(connection)
raise 'API connections must be instances of JSS::APIConnection' unless connection.is_a? JSS::APIConnection
+
@api = connection
end
# Make the default connection (Stored in JSS::API) active
#