lib/jss/api_connection.rb in ruby-jss-0.8.2 vs lib/jss/api_connection.rb in ruby-jss-0.9.0.b1
- old
+ new
@@ -35,23 +35,134 @@
#####################################
# Classes
#####################################
- # An API connection to the JSS.
+ # Instances of this class represent an API connection to the JSS.
#
- # This is a singleton class, only one can exist at a time.
- # Its one instance is created automatically when the module loads, but it
- # isn't connected to anything at that time.
+ # JSS::APIConnection objects are REST connections to JSS APIs and contain
+ # (once connected) all the data needed for communication with
+ # that API, including login credentials, URLs, and so on.
#
- # Use it via the {JSS::API} constant to call the #connect
- # method, and the {#get_rsrc}, {#put_rsrc}, {#post_rsrc}, & {#delete_rsrc}
- # methods, q.v. below.
+ # == The default connection
#
- # To access the underlying RestClient::Resource instance,
- # use JSS::API.cnx
+ # When ruby-jss is loaded, a not-yet-connected default instance of
+ # JSS::APIConnection is created, activated, and stored internally.
+ # Before using it you must call its {#connect} method, passing in appropriate
+ # connection details and credentials.
#
+ # Here's how to use the default connection:
+ #
+ # require 'ruby-jss'
+ # JSS.api.connect server: 'server.address.edu', user: 'jss-api-user', pw: :prompt
+ #
+ # (see {JSS::APIConnection#connect} for all the connection options)
+ #
+ # If you're only going to be connecting to one server, or one at a time,
+ # using the default connection is preferred. You can call its {#connect}
+ # method at any time to change servers or connection credentials.
+ #
+ # == Multiple connections & the currently active connection
+ #
+ # Sometimes you need to connect simultaneously to more than one JSS.
+ # or to the same JSS with different credentials.
+ #
+ # While multiple connection instances can be created, only one is active at
+ # a time and all API access happens through the currently active connection.
+ # (See below for how to switch between different connections)
+ #
+ # The currently-active connection instance is available from the
+ # `JSS.api` method.
+ #
+ # == Making new connection instances
+ #
+ # New connections can be created and stored in a variable using
+ # the standard ruby 'new' method.
+ #
+ # If you provide connection details when calling 'new', they will be passed
+ # to the #connect method immediately.
+ #
+ # production_api = JSS::APIConnection.new(
+ # name: 'prod',
+ # server: 'prodserver.address.org',
+ # user: 'produser',
+ # pw: :prompt
+ # )
+ #
+ # # the new connection is now stored in the variable 'production_api'.
+ #
+ # == Switching between multiple connections
+ #
+ # Only one connection is active at a time and the currently active one is
+ # returned when you call `JSS.api` or its aliases `JSS.api_connection` or
+ # `JSS.connection`
+ #
+ # To activate another connection just pass it to the JSS.use_api method like so:
+ # JSS.use_api production_api
+ # # the connection we stored in 'production_api' is now active
+ #
+ # To re-activate to the default connection, just call
+ # JSS.use_default_connection
+ #
+ # NOTE:
+ # The APIObject list methods (e.g. JSS::Computer.all) cache the list
+ # data from the API the first time they are used, and after that when
+ # their 'refresh' option is true.
+ #
+ # Those caches are stored in the APIConnection instance through-
+ # which they were read, so they won't be incorrect when you switch
+ # connections.
+ #
+ # == Connection Names:
+ #
+ # As seen in the example above, you can provide a 'name:' parameter
+ # (a String or a Symbol) when creating a new connection. The name can be
+ # used later to identify connection objects.
+ #
+ # If you don't provide one, the name is ':disconnected' until you
+ # connect, and then 'user@server:port' after connecting.
+ #
+ # The name of the default connection is always :default
+ #
+ # To see the name of the currently active connection, just use `JSS.api.name`
+ #
+ # JSS.use_api production_api
+ # JSS.api.name # => 'prod'
+ #
+ # JSS.use_default_connection
+ # JSS.api.name # => :default
+ #
+ # == Creating, Storing and Activating a connection in one step
+ #
+ # Both of the above steps (creating/storing a connection, and making it
+ # active) can be performed in one step using the
+ # `JSS.new_api_connection` method, which creates a new APIConnection, makes it
+ # the active connection, and returns it.
+ #
+ # production_api2 = JSS.new_api_connection(
+ # name: 'prod2',
+ # server: 'prodserver.address.org',
+ # user: 'produser',
+ # pw: :prompt
+ # )
+ #
+ # JSS.api.name # => 'prod2'
+ #
+ # == Low-level use of APIConnection instances.
+ #
+ # For most uses, creating, activating, and connecting APIConnection instances
+ # 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
+ # 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.
+ #
class APIConnection
# Class Constants
#####################################
@@ -115,17 +226,45 @@
attr_reader :last_http_response
# @return [String] The base URL to to the current REST API
attr_reader :rest_url
+ # @return [String,Symbol] an arbitrary name that can be given to this
+ # connection during initialization, using the name: parameter.
+ # defaults to user@hostname:port
+ attr_reader :name
+
+ # @return [Hash]
+ # This Hash holds the most recent API query for a list of all items in any
+ # APIObject subclass, keyed by the subclass's RSRC_LIST_KEY.
+ # See the APIObject.all class method.
+ #
+ # When the APIObject.all method is called without an argument,
+ # and this hash has a matching value, the value is returned, rather than
+ # requerying the API. The first time a class calls .all, or whnever refresh
+ # is not false, the API is queried and the value in this hash is updated.
+ attr_reader :object_list_cache
+
# Constructor
#####################################
- # To connect, use JSS::API.connect
+ # If name: is provided (as a String or Symbol) that will be
+ # stored as the APIConnection's name attribute.
#
- def initialize
+ # For other available parameters, see {#connect}.
+ #
+ # If they are provided, they will be used to establish the
+ # connection immediately.
+ #
+ # If not, you must call {#connect} before accessing the API.
+ #
+ def initialize(args = {})
+ @name = args.delete :name
+ @name ||= :disconnected
@connected = false
+ @object_list_cache = {}
+ connect args unless args.empty?
end # init
# Instance Methods
#####################################
@@ -173,10 +312,11 @@
# heres our connection
@cnx = RestClient::Resource.new(@rest_url.to_s, args)
verify_server_version
+ @name = "#{@jss_user}@#{@server_host}:#{@port}" if @name.nil? || @name == :disconnected
@connected ? hostname : nil
end # connect
# A useful string about this connection
#
@@ -235,11 +375,12 @@
# If the second argument is :xml, the XML data is returned as a String.
#
# @return [Hash,String] the result of the get
#
def get_rsrc(rsrc, format = :json)
- raise JSS::InvalidConnectionError, 'Not Connected. Use JSS::API.connect first.' unless @connected
+ # puts object_id
+ raise JSS::InvalidConnectionError, 'Not Connected. Use JSS.api.connect first.' unless @connected
rsrc = URI.encode rsrc
@last_http_response = @cnx[rsrc].get(accept: format)
return JSON.parse(@last_http_response, symbolize_names: true) if format == :json
end
@@ -250,11 +391,11 @@
# @param xml[String] the xml specifying the changes.
#
# @return [String] the xml response from the server.
#
def put_rsrc(rsrc, xml)
- raise JSS::InvalidConnectionError, 'Not Connected. Use JSS::API.connect first.' unless @connected
+ raise JSS::InvalidConnectionError, 'Not Connected. Use JSS.api_connection.connect first.' unless @connected
# convert CRs & to
xml.gsub!(/\r/, ' ')
# send the data
@@ -269,15 +410,15 @@
#
# @param xml[String] the xml specifying the new object.
#
# @return [String] the xml response from the server.
#
- def post_rsrc(rsrc, xml)
- raise JSS::InvalidConnectionError, 'Not Connected. Use JSS::API.connect first.' unless @connected
+ def post_rsrc(rsrc, xml = '')
+ raise JSS::InvalidConnectionError, 'Not Connected. Use JSS.api_connection.connect first.' unless @connected
# convert CRs & to
- xml.gsub!(/\r/, ' ')
+ xml.gsub!(/\r/, ' ') if xml
# send the data
@last_http_response = @cnx[rsrc].post xml, content_type: 'text/xml', accept: :json
rescue RestClient::Conflict => exception
raise_conflict_error(exception)
@@ -287,14 +428,17 @@
#
# @param rsrc[String] the resource to create, the URL part after 'JSSResource/'
#
# @return [String] the xml response from the server.
#
- def delete_rsrc(rsrc)
- raise JSS::InvalidConnectionError, 'Not Connected. Use JSS::API.connect first.' unless @connected
+ def delete_rsrc(rsrc, xml = nil)
+ raise JSS::InvalidConnectionError, 'Not Connected. Use JSS.api_connection.connect first.' unless @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
end # delete_rsrc
# Test that a given hostname & port is a JSS API server
@@ -350,18 +494,33 @@
# Private Insance Methods
####################################
private
- # Apply defaults from the JSS::CONFIG, the JSS::Client
- # or the module defaults to the args for the #connect method
+ # Apply defaults from the JSS::CONFIG,
+ # then from the JSS::Client,
+ # then from the module defaults
+ # to the args for the #connect method
#
# @param args[Hash] The args for #connect
#
# @return [Hash] The args with defaults applied
#
def apply_connection_defaults(args)
+ apply_defaults_from_config(args)
+ apply_defaults_from_client(args)
+ apply_module_defaults(args)
+ end
+
+ # Apply defaults from the JSS::CONFIG
+ # to the args for the #connect method
+ #
+ # @param args[Hash] The args for #connect
+ #
+ # @return [Hash] The args with defaults applied
+ #
+ def apply_defaults_from_config(args)
# settings from config if they aren't in the args
args[:server] ||= JSS::CONFIG.api_server_name
args[:port] ||= JSS::CONFIG.api_server_port
args[:user] ||= JSS::CONFIG.api_username
args[:timeout] ||= JSS::CONFIG.api_timeout
@@ -369,16 +528,36 @@
args[:ssl_version] ||= JSS::CONFIG.api_ssl_version
# if verify cert was not in the args, get it from the prefs.
# We can't use ||= because the desired value might be 'false'
args[:verify_cert] = JSS::CONFIG.api_verify_cert if args[:verify_cert].nil?
+ args
+ end # apply_defaults_from_config
+ # Apply defaults from the JSS::Client
+ # to the args for the #connect method
+ #
+ # @param args[Hash] The args for #connect
+ #
+ # @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
- args[:use_ssl] ||= JSS::Client.jss_protocol.end_with? 's'
+ args[:use_ssl] ||= JSS::Client.jss_protocol.to_s.end_with? 's'
+ args
+ end
+ # Apply the module defaults to the args for the #connect method
+ #
+ # @param args[Hash] The args for #connect
+ #
+ # @return [Hash] The args with defaults applied
+ #
+ def apply_module_defaults(args)
# defaults from the module if needed
args[:port] ||= args[:use_ssl] ? SSL_PORT : HTTP_PORT
args[:timeout] ||= DFT_TIMEOUT
args[:open_timeout] ||= DFT_OPEN_TIMEOUT
args[:ssl_version] ||= DFT_SSL_VERSION
@@ -408,11 +587,22 @@
#
# @return [void]
#
def verify_server_version
@connected = true
- @server = JSS::Server.new
+
+ # the jssuser resource is readable by anyone with a JSS acct
+ # 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]
+ rescue RestClient::Unauthorized, RestClient::Request::Unauthorized
+ raise JSS::AuthenticationError, "Incorrect JSS username or password for '#{JSS.api_connection.jss_user}@#{JSS.api_connection.server_host}'."
+ end
+
min_vers = JSS.parse_jss_version(JSS::MINIMUM_SERVER_VERSION)[:version]
return unless @server.version < min_vers
err_msg = "JSS version #{@server.raw_version} to low. Must be >= #{min_vers}"
@connected = false
raise JSS::UnsupportedError, err_msg
@@ -483,11 +673,103 @@
conflict_reason = Regexp.last_match(1)
conflict_reason ||= exception.http_body
raise JSS::ConflictError, conflict_reason
end
- end # class JSSAPIConnection
+ # 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)
+ )
+ end # delete_with_payload
- # The default APIConnection
- API = APIConnection.new
+ end # class APIConnection
+
+ # 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.
+ #
+ # @param (See JSS::APIConnection#connect)
+ #
+ # @return [APIConnection] the new, active connection
+ #
+ def self.new_api_connection(args = {})
+ @api = APIConnection.new args
+ @api
+ end
+
+ # Switch the connection used for all API interactions to the
+ # one provided. See {JSS::APIConnection} for details and examples
+ # of using multiple connections
+ #
+ # @param connection [APIConnection] The APIConnection to use for future
+ # API calls. If omitted, use the default connection created when ruby-jss
+ # was loaded (which may or may not yet be connected)
+ #
+ # @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
+ #
+ # @return [void]
+ #
+ def self.use_default_connection
+ use_api_connection API
+ end
+
+ # The currently active JSS::APIConnection instance.
+ #
+ # @return [JSS::APIConnection]
+ #
+ def self.api
+ @api
+ end
+
+ # aliases of module methods
+ class << self
+ alias api_connection api
+ alias connection api
+
+ alias new_connection new_api_connection
+ alias new_api new_api_connection
+
+ alias use_api use_api_connection
+ alias use_connection use_api_connection
+ end
+
+ # create the default connection
+ new_api_connection(name: :default) unless @api
+
+ # Save the default connection in the API constant,
+ # mostly for backward compatibility.
+ API = @api unless defined? API
end # module