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 &#13; xml.gsub!(/\r/, '&#13;') # 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 &#13; - xml.gsub!(/\r/, '&#13;') + xml.gsub!(/\r/, '&#13;') 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