lib/jss/api_connection.rb in ruby-jss-0.9.2 vs lib/jss/api_connection.rb in ruby-jss-0.10.0a1

- old
+ new

@@ -35,85 +35,147 @@ ##################################### # Classes ##################################### - # Instances of this class represent an API connection to the JSS. + # Instances of this class represent a REST connection to a JSS API. # - # 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. + # For most cases, a single connection to a single JSS is all you need, and + # this is ruby-jss's default behavior. # - # == The default connection + # If needed, multiple connections can be made and used sequentially or + # simultaneously. # + # == Using the default connection + # # When ruby-jss is loaded, a not-yet-connected default instance of - # JSS::APIConnection is created, activated, and stored internally. + # JSS::APIConnection is created and stored in the constant JSS::API. + # This connection is used as the initial 'active connection' (see below) + # so all methods that make API calls will use it by default. For most uses, + # where you're only going to be working with one connection to one JSS, the + # default connection is all you need. + # # Before using it you must call its {#connect} method, passing in appropriate # connection details and credentials. # - # Here's how to use the default connection: + # Example: # - # require 'ruby-jss' - # JSS.api.connect server: 'server.address.edu', user: 'jss-api-user', pw: :prompt + # 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) # - # (see {JSS::APIConnection#connect} for all the connection options) + # a_phone = JSS::MobileDevice.fetch id: 8743 # - # 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. + # # the mobile device was fetched through the default connection # - # == Multiple connections & the currently active connection + # == Using Multiple Simultaneous Connections # # Sometimes you need to connect simultaneously to more than one JSS. - # or to the same JSS with different credentials. + # or to the same JSS with different credentials. ruby-jss allows you to + # create as many connections as needed, and gives you three ways to use them: # - # 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) + # 1. Making a connection 'active', after which API calls go thru it + # automatically # - # The currently-active connection instance is available from the - # `JSS.api` method. + # Example: # - # == Making new connection instances + # a_computer = JSS::Computer.fetch id: 1234 # - # New connections can be created and stored in a variable using - # the standard ruby 'new' method. + # # the JSS::Computer with id 1234 is fetched from the active connection + # # and stored in the variable 'a_computer' # + # NOTE: When ruby-jss is first loaded, the default connection (see above) + # is the active connection. + # + # 2. Passing an APIConnection instance to methods that use the API + # + # Example: + # + # a_computer = JSS::Computer.fetch id: 1234, api: production_api + # + # # the JSS::Computer with id 1234 is fetched from the connection + # # stored in the variable 'production_api'. The computer is + # # then stored in the variable 'a_computer' + # + # 3. Using the APIConnection instance itself to make API calls. + # + # Example: + # + # a_computer = production_api.fetch :Computer, id: 1234 + # + # # the JSS::Computer with id 1234 is fetched from the connection + # # stored in the variable 'production_api'. The computer is + # # then stored in the variable 'a_computer' + # + # See below for more details about the ways to use multiple connections. + # + # NOTE: + # Objects retrieved or created through an APIConnection store an internal + # reference to that APIConnection and use that when they make other API + # calls, thus ensuring data consistency when using multiple connections. + # + # Similiarly, the data caches used by APIObject list methods (e.g. + # JSS::Computer.all, .all_names, and so on) are stored in the APIConnection + # instance through which they were read, so they won't be incorrect when + # you use multiple connections. + # + # == Making new APIConnection instances + # + # New connections can be created using the standard ruby 'new' method. + # # If you provide connection details when calling 'new', they will be passed - # to the #connect method immediately. + # to the {#connect} method immediately. Otherwise you can call {#connect} later. # # 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 + # == Using the 'Active' Connection # - # 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` + # While multiple connection instances can be created, only one at a time is + # 'the active connection' and all APIObject-based access methods in ruby-jss + # will use it automatically. When ruby-jss is loaded, the default connection + # (see above) is the active connection. # + # To use the active connection, just call a method on an APIObject subclass + # that uses the API. + # + # For example, the various list methods: + # + # all_computer_sns = JSS::Computer.all_serial_numbers + # + # # the list of all computer serial numbers is read from the active + # # connection and stored in all_computer_sns + # + # Fetching an object from the API: + # + # victim_md = JSS::MobileDevice.fetch id: 832 + # + # # the variable 'victim_md' now contains a JSS::MobileDevice queried + # # through the active connection. + # + # The currently-active connection instance is available from the + # `JSS.api` method. + # + # === Making a Connection Active + # + # Only one connection is 'active' at a time and the currently active one is + # returned when you call `JSS.api` or its alias `JSS.active_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. @@ -145,13 +207,65 @@ # pw: :prompt # ) # # JSS.api.name # => 'prod2' # + # == Passing an APIConnection object to API-related methods + # + # All methods that use the API can take an 'api:' parameter which + # contains an APIConnection object. When provided, that APIconnection is + # used rather than the active connection. + # + # For example: + # + # prod2_computer_sns = JSS::Computer.all_serial_numbers, api: production_api2 + # + # # the list of all computer serial numbers is read from the connection in + # # the variable 'production_api2' and stored in 'prod2_computer_sns' + # + # prod2_victim_md = JSS::MobileDevice.fetch id: 832, api: production_api2 + # + # # the variable 'prod2_victim_md' now contains a JSS::MobileDevice queried + # # through the connection 'production_api2'. + # + # == Using the APIConnection itself to make API calls. + # + # Rather than passing an APIConnection into another method, you can call + # similar methods on the connection itself. For example, these two calls + # have the same result as the two examples above: + # + # prod2_computer_sns = production_api2.all :Computer, only: :serial_numbers + # prod2_victim_md = production_api2.fetch :MobileDevice, id: 832 + # + # Here are the API calls you can make directly from an APIConnection object. + # Most of them behave identically to the same methods in the APIObject classes + # + # - {#all} The 'list' methods of the various APIObject classes. Use the 'only:' + # parameter to specify one of the sub-list-methods, like #all_ids or + # #all_laptops + # - {#map_all_ids} the equivalent of #map_all_ids_to in the APIObject classes + # - {#valid_id} given a class and an identifier (like macaddress or udid) + # return a valid id or nil + # - {#exist?} given a class and an identifier (like macaddress or udid) does + # the identifier exist for the class in the JSS + # - {#match} list items in the JSS matching a query + # (if the object is {Matchable}) + # - {#fetch} retrieve an object from the JSS + # - {#make} instantiate an object to be created in the JSS + # - {#send_computer_mdm_command} same as {Computer.send_mdm_command} + # - {#computer_checkin_settings} same as {Computer.checkin_settings} + # - {#computer_inventory_collection_settings} same as {Computer.inventory_collection_settings} + # - {#send_mobiledevice_mdm_command} same as {MobileDevice.send_mdm_command} + # - {#master_distribution_point} same as {DistributionPoint.master_distribution_point} + # - {#my_distribution_point} same as {DistributionPoint.my_distribution_point} + # - {#network_ranges} same as {NetworkSegment.network_ranges} + # - {#network_segments_for_ip} same as {NetworkSegment.segments_for_ip} + # - {#my_network_segments} same as {NetworkSegment.my_network_segments} + # # == Low-level use of APIConnection instances. # - # For most uses, creating, activating, and connecting APIConnection instances + # For most cases, using APIConnection instances as mentioned above # 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. # @@ -376,11 +490,11 @@ # # @return [Hash,String] the result of the get # def get_rsrc(rsrc, format = :json) # puts object_id - raise JSS::InvalidConnectionError, 'Not Connected. Use JSS.api.connect first.' unless @connected + raise JSS::InvalidConnectionError, 'Not Connected. Use .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 @@ -391,11 +505,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_connection.connect first.' unless @connected + raise JSS::InvalidConnectionError, 'Not Connected. Use .connect first.' unless @connected # convert CRs & to &#13; xml.gsub!(/\r/, '&#13;') # send the data @@ -411,11 +525,11 @@ # @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_connection.connect first.' unless @connected + raise JSS::InvalidConnectionError, 'Not Connected. Use .connect first.' unless @connected # convert CRs & to &#13; xml.gsub!(/\r/, '&#13;') if xml # send the data @@ -429,11 +543,11 @@ # @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) - raise JSS::InvalidConnectionError, 'Not Connected. Use JSS.api_connection.connect first.' unless @connected + raise JSS::InvalidConnectionError, 'Not Connected. Use .connect first.' unless @connected raise MissingDataError, 'Missing :rsrc' if rsrc.nil? # payload? return delete_with_payload rsrc, xml if xml @@ -490,10 +604,278 @@ # aliases alias connected? connected alias host hostname + + ################# + + # Call one of the 'all*' methods on a JSS::APIObject subclass + # using this APIConnection. + # + # @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. + # + # @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, api: self + end + + # Call the 'valid_id' method on a JSS::APIObject subclass + # using this APIConnection. See {JSS::APIObject.valid_id} + # + # @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?} + # + # @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} + # + # @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} + # + # @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} + # + # @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} passing this API + # connection + # + # @return (see JSS::Computer.checkin_settings) + # + def computer_checkin_settings + JSS::Computer.checkin_settings api: self + end + + + # Call {JSS::Computer.inventory_collection_settings} passing this API + # connection + # + # @return (see JSS::Computer.checkin_settings) + # + def computer_inventory_collection_settings + JSS::Computer.inventory_collection_settings 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) + @master_distribution_point = nil if refresh + return @master_distribution_point if @master_distribution_point + + all_dps = JSS::DistributionPoint.all refresh, api: self + + @master_distribution_point = + case all_dps.size + when 0 + raise JSS::NoSuchItemError, "No distribution points defined" + when 1 + JSS::DistributionPoint.fetch id: all_dps.first[:id], api: self + else + JSS::DistributionPoint.fetch id: :master, api: self + end + 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) + @my_distribution_point = nil if refresh + return @my_distribution_point if @my_distribution_point + + my_net_seg = my_network_segments[0] + @my_distribution_point = JSS::NetworkSegment.fetch(id: my_net_seg, api: self).distribution_point if my_net_seg + @my_distribution_point ||= master_distribution_point refresh + @my_distribution_point + end + + # All NetworkSegments in this jss as IPAddr object Ranges representing the + # Segment, e.g. with starting = 10.24.9.1 and ending = 10.24.15.254 + # the range looks like: + # <IPAddr: IPv4:10.24.9.1/255.255.255.255>..#<IPAddr: IPv4:10.24.15.254/255.255.255.255> + # + # Using the #include? method on those Ranges is very useful. + # + # @param refresh[Boolean] should the data be re-queried? + # + # @return [Hash{Integer => Range}] the network segments as IPv4 address Ranges + # + def network_ranges(refresh = false) + @network_ranges = nil if refresh + return @network_ranges if @network_ranges + @network_ranges = {} + JSS::NetworkSegment.all(refresh, api: self).each do |ns| + @network_ranges[ns[:id]] = IPAddr.new(ns[:starting_address])..IPAddr.new(ns[:ending_address]) + end + @network_ranges + end # def network_segments + + # Find the ids of the network segments that contain a given IP address. + # + # Even tho IPAddr.include? will take a String or an IPAddr + # I convert the ip to an IPAddr so that an exception will be raised if + # the ip isn't a valid ip. + # + # @param ip[String, IPAddr] the IP address to locate + # + # @param refresh[Boolean] should the data be re-queried? + # + # @return [Array<Integer>] the ids of the NetworkSegments containing the given ip + # + def network_segments_for_ip(ip, refresh = false) + ok_ip = IPAddr.new(ip) + matches = [] + network_ranges.each { |id, subnet| matches << id if subnet.include?(ok_ip) } + matches + end + + # Find the current network segment ids for the machine running this code + # + # @return [Array<Integer>] the NetworkSegment ids for this machine right now. + # + def my_network_segments + network_segments_for_ip JSS::Client.my_ip_address + end + + # Send an MDM command to one or more computers managed by + # this JSS + # + # see {JSS::Computer.send_mdm_command} + # + def send_computer_mdm_command(targets, command, passcode = nil) + JSS::Computer.send_mdm_command(targets, command, passcode, api: self) + end + + # Send an MDM command to one or more mobile devices managed by + # this JSS + # + # see {JSS::MobileDevice.send_mdm_command} + # + def send_mobiledevice_mdm_command(targets, command, data = nil) + JSS::MobileDevice.send_mdm_command(targets, command, data, api: self) + end + + # Remove the various cached data + # from the instance_variables used to create + # pretty-print (pp) output. + # + # @return [Array] the desired instance_variables + # + def pretty_print_instance_variables + vars = instance_variables.sort + vars.delete :@object_list_cache + vars.delete :@last_http_response + vars.delete :@network_ranges + vars.delete :@my_distribution_point + vars.delete :@master_distribution_point + vars + end + # Private Insance Methods #################################### private # Apply defaults from the JSS::CONFIG, @@ -594,13 +976,13 @@ # 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] + @server = JSS::Server.new get_rsrc('jssuser')[:user], self 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}'." + raise JSS::AuthenticationError, "Incorrect JSS username or password for '#{@jss_user}@#{@server_host}:#{@port}'." 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}" @@ -755,15 +1137,17 @@ # aliases of module methods class << self alias api_connection api alias connection api + alias active_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 + alias activate_connection use_api_connection end # create the default connection new_api_connection(name: :default) unless @api