Class: JSS::APIConnection

Inherits:
Object show all
Defined in:
lib/jss.rb,
lib/jss/api_connection.rb

Overview

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.

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 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.

Example:

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)

a_phone = JSS::MobileDevice.fetch id: 8743

# the mobile device was fetched through the default connection

Using Multiple Simultaneous Connections

Sometimes you need to connect simultaneously to more than one JSS. 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:

  1. Making a connection 'active', after which API calls go thru it automatically

    Example:

    a_computer = JSS::Computer.fetch id: 1234
    
    # 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. 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'.

Using the 'Active' 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

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'

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'.

Low-level use of 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.

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 Server q.v., representing the JSS to which it's connected.

Constant Summary collapse

RSRC_BASE =

The base API path in the jss URL

'JSSResource'.freeze
TEST_PATH =

A url path to load to see if there's an API available at a host. This just loads the API resource docs page

"#{RSRC_BASE}/accounts".freeze
TEST_CONTENT =

If the test path loads correctly from a casper server, it'll contain this text (this is what we get when we make an unauthenticated API call.)

'<p>The request requires user authentication</p>'.freeze
HTTP_PORT =

The Default port

9006
SSL_PORT =

The Jamf default SSL port, default for locally-hosted servers

8443
HTTPS_SSL_PORT =

The https default SSL port, default for Jamf Cloud servers

443
SSL_PORTS =

if either of these is specified, we'll default to SSL

[SSL_PORT, HTTPS_SSL_PORT].freeze
JAMFCLOUD_DOMAIN =

Recognize Jamf Cloud servers

'jamfcloud.com'.freeze
JAMFCLOUD_PORT =

JamfCloud connections default to 443, not 8443

HTTPS_SSL_PORT
XML_HEADER =

The top line of an XML doc for submitting data via API

'<?xml version="1.0" encoding="UTF-8" standalone="no"?>'.freeze
DFT_OPEN_TIMEOUT =

Default timeouts in seconds

60
DFT_TIMEOUT =
60
DFT_SSL_VERSION =

The Default SSL Version

'TLSv1_2'.freeze
RSRC_NOT_FOUND_MSG =
'The requested resource was not found'.freeze
EXTENDABLE_CLASSES =

These classes are extendable, and may need cache flushing for EA definitions

[JSS::Computer, JSS::MobileDevice, JSS::User].freeze
GET_FORMATS =

values for the format param of get_rsrc

%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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ APIConnection

If name: is provided (as a String or Symbol) that will be stored as the APIConnection's name attribute.

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.



385
386
387
388
389
390
391
# File 'lib/jss/api_connection.rb', line 385

def initialize(args = {})
  @name = args.delete :name
  @name ||= :unknown
  @connected = false
  @object_list_cache = {}
  connect args unless args.empty?
end

Instance Attribute Details

#cnxFaraday::Connection (readonly)

Returns the underlying connection resource.

Returns:

  • (Faraday::Connection)

    the underlying connection resource



299
300
301
# File 'lib/jss/api_connection.rb', line 299

def cnx
  @cnx
end

#connectedBoolean (readonly) Also known as: connected?

Returns are we connected right now?.

Returns:

  • (Boolean)

    are we connected right now?



302
303
304
# File 'lib/jss/api_connection.rb', line 302

def connected
  @connected
end

#ext_attr_definition_cacheHash{Class: Hash{String => JSS::ExtensionAttribute}} (readonly)

This Hash caches the Extension Attribute definition objects for the three types of ext. attribs: ComputerExtensionAttribute, MobileDeviceExtensionAttribute, and UserExtensionAttribute, whenever they are fetched for parsing or validating extention attribute data.

The top-level keys are the EA classes themselves:

  • ComputerExtensionAttribute

  • MobileDeviceExtensionAttribute

  • UserExtensionAttribute

These each point to a Hash of their instances, keyed by name, e.g.

{
 "A Computer EA" => <JSS::ComputerExtensionAttribute...>,
 "A different Computer EA" => <JSS::ComputerExtensionAttribute...>,
 ...
}

Returns:



370
371
372
# File 'lib/jss/api_connection.rb', line 370

def ext_attr_definition_cache
  @ext_attr_definition_cache
end

#last_http_responseFaraday::Response (readonly)

Returns The response from the most recent API call.

Returns:

  • (Faraday::Response)

    The response from the most recent API call



321
322
323
# File 'lib/jss/api_connection.rb', line 321

def last_http_response
  @last_http_response
end

#nameString, Symbol (readonly)

connection during initialization, using the name: parameter. defaults to user@hostname:port

Returns:

  • (String, Symbol)

    an arbitrary name that can be given to this



329
330
331
# File 'lib/jss/api_connection.rb', line 329

def name
  @name
end

#object_list_cacheHash (readonly)

This Hash caches the result of the the first API query for an APIObject subclass's .all summary list, keyed by the subclass's RSRC_LIST_KEY. See the APIObject.all class method.

It also holds related data items for speedier processing:

  • The Hashes created by APIObject.map_all_ids_to(foo), keyed by “#RSRC_LIST_KEYmap#other_key”.to_sym

  • This hash also holds a cache of the rarely-used APIObject.all_objects hash, keyed by “#RSRC_LIST_KEY_objects”.to_sym

When APIObject.all, and related methods are 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.

Returns:



349
350
351
# File 'lib/jss/api_connection.rb', line 349

def object_list_cache
  @object_list_cache
end

#portInteger (readonly)

Returns the port used for the connection.

Returns:

  • (Integer)

    the port used for the connection



315
316
317
# File 'lib/jss/api_connection.rb', line 315

def port
  @port
end

#protocolString (readonly)

Returns the protocol being used: http or https.

Returns:

  • (String)

    the protocol being used: http or https



318
319
320
# File 'lib/jss/api_connection.rb', line 318

def protocol
  @protocol
end

#rest_urlString (readonly)

Returns The base URL to to the current REST API.

Returns:

  • (String)

    The base URL to to the current REST API



324
325
326
# File 'lib/jss/api_connection.rb', line 324

def rest_url
  @rest_url
end

#serverJSS::Server (readonly)

Returns the details of the JSS to which we're connected.

Returns:

  • (JSS::Server)

    the details of the JSS to which we're connected.



306
307
308
# File 'lib/jss/api_connection.rb', line 306

def server
  @server
end

#server_hostString (readonly)

Returns the hostname of the JSS to which we're connected.

Returns:

  • (String)

    the hostname of the JSS to which we're connected.



309
310
311
# File 'lib/jss/api_connection.rb', line 309

def server_host
  @server_host
end

#server_pathString (readonly)

Returns any path in the URL below the hostname. See #connect.

Returns:



312
313
314
# File 'lib/jss/api_connection.rb', line 312

def server_path
  @server_path
end

#userString (readonly) Also known as: jss_user

Returns the username who's connected to the JSS API.

Returns:

  • (String)

    the username who's connected to the JSS API



295
296
297
# File 'lib/jss/api_connection.rb', line 295

def user
  @user
end

Instance Method Details

#connect(args = {}) ⇒ true

Connect to the JSS Classic API.

Parameters:

  • args (Hash) (defaults to: {})

    the keyed arguments for connection.

Options Hash (args):

  • :server (String)

    the hostname of the JSS API server, required if not defined in JSS::CONFIG

  • :server_path (String)

    If your JSS is not at the root of the server, e.g. if it's at

    https://myjss.myserver.edu:8443/dev_mgmt/jssweb
    

    rather than

    https://myjss.myserver.edu:8443/
    

    then use this parameter to specify the path below the root e.g:

    server_path: 'dev_mgmt/jssweb'
    
  • :port (Integer)

    the port number to connect with, defaults to 8443

  • :use_ssl (Boolean)

    should the connection be made over SSL? Defaults to true.

  • :verify_cert (Boolean)

    should HTTPS SSL certificates be verified. Defaults to true.

  • :user (String)

    a JSS user who has API privs, required if not defined in JSS::CONFIG

  • :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. If :stdin#, the password is read from a line of std in represented by the digit at #, so :stdin3 reads the passwd from the third line of standard input. defaults to line 1, if no digit is supplied. see JSS.stdin

  • :open_timeout (Integer)

    the number of seconds to wait for an initial response, defaults to 60

  • :timeout (Integer)

    the number of seconds before an API call times out, defaults to 60

Returns:

  • (true)


430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/jss/api_connection.rb', line 430

def connect(args = {})
  # new connections always get new caches
  flushcache

  args[:no_port_specified] = args[:port].to_s.empty?
  args = apply_connection_defaults args
  @timeout = args[:timeout]
  @open_timeout = args[:open_timeout]

  # ensure an integer
  args[:port] &&= args[:port].to_i

  # confirm we know basics
  verify_basic_args args

  # parse our ssl situation
  verify_ssl args

  @user = args[:user]

  @rest_url = build_rest_url args

  # figure out :password from :pw
  args[:password] = acquire_password args

  # heres our connection
  @cnx = create_connection args[:password]

  verify_server_version

  @name = "#{@user}@#{@server_host}:#{@port}" if @name.nil? || @name == :disconnected
  @connected ? hostname : nil
end

#delete_rsrc(rsrc) ⇒ String

Delete a resource from the JSS

Parameters:

  • rsrc (String)

    the resource to create, the URL part after 'JSSResource/'

Returns:

  • (String)

    the xml response from the server.

Raises:



648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
# File 'lib/jss/api_connection.rb', line 648

def delete_rsrc(rsrc)
  validate_connected
  raise MissingDataError, 'Missing :rsrc' if rsrc.nil?

  # delete the resource
  @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
end

#disconnectvoid

This method returns an undefined value.

With a REST connection, there isn't any real “connection” to disconnect from So to disconnect, we just unset all our credentials.



497
498
499
500
501
502
503
# File 'lib/jss/api_connection.rb', line 497

def disconnect
  @user = nil
  @rest_url = nil
  @server_host = nil
  @cnx = nil
  @connected = false
end

#flushcache(key = nil) ⇒ void

This method returns an undefined value.

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)

NOTE if you've referenced objects in these caches, those objects won't be removed from memory, but all cached data will be recached as needed.

Parameters:

  • key (Symbol, Class) (defaults to: nil)

    Flush only the caches for the given RSRC_LIST_KEY. or the EAdef cache for the given extendable class. If nil (the default) flushes all caches



715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'lib/jss/api_connection.rb', line 715

def flushcache(key = nil)
  if EXTENDABLE_CLASSES.include? key
    @ext_attr_definition_cache[key] = {}
  elsif key
    map_key_pfx = "#{key}_map_"
    @object_list_cache.delete_if do |cache_key, _cache|
      cache_key == key || cache_key.to_s.start_with?(map_key_pfx)
    end
    @ext_attr_definition_cache
  else
    @object_list_cache = {}
    @ext_attr_definition_cache = {}
  end

  GC.start
end

#get_rsrc(rsrc, format = :json, raw_json: false) ⇒ Hash, String

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 with symbolized Hash keys.

If the second parameter is :xml then the XML version is retrieved and returned as a String.

To get the raw JSON string as it comes from the API, pass raw_json: true

Parameters:

  • rsrc (String)

    the resource to get (the part of the API url after the 'JSSResource/' )

  • format (Symbol) (defaults to: :json)

    either ;json or :xml If the second argument is :xml, the XML data is returned as a String.

  • raw_json (Boolean) (defaults to: false)

    When GETting JSON, return the raw unparsed string (the XML is always returned as a raw string)

Returns:

Raises:



529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'lib/jss/api_connection.rb', line 529

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

  @last_http_response =
    @cnx.get(rsrc) do |req|
      req.headers[HTTP_ACCEPT_HEADER] = format == :json ? MIME_JSON : MIME_XML
    end

  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

#hostnameString Also known as: host

The server to which we are connected, or will try connecting to if none is specified with the call to #connect

Returns:

  • (String)

    the hostname of the server



690
691
692
693
694
695
696
# File 'lib/jss/api_connection.rb', line 690

def hostname
  return @server_host if @server_host

  srvr = JSS::CONFIG.api_server_name
  srvr ||= JSS::Client.jss_server
  srvr
end

#open_timeout=(timeout) ⇒ void

This method returns an undefined value.

Reset the open-connection timeout for the rest connection

Parameters:

  • timeout (Integer)

    the new timeout in seconds



488
489
490
# File 'lib/jss/api_connection.rb', line 488

def open_timeout=(timeout)
  @cnx.options[:open_timeout] = timeout
end

#post_rsrc(rsrc, xml) ⇒ String

Create a new JSS resource

Parameters:

  • rsrc (String)

    the API resource being created, the URL part after 'JSSResource/'

  • xml (String)

    the xml specifying the new object.

Returns:

  • (String)

    the xml response from the server.



585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'lib/jss/api_connection.rb', line 585

def post_rsrc(rsrc, xml)
  validate_connected

  # convert CRs & to &#13;
  xml&.gsub!(/\r/, '&#13;')

  # send the data
  @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
end

#pretty_print_instance_variablesArray

Remove the various cached data from the instance_variables used to create pretty-print (pp) output.

Returns:

  • (Array)

    the desired instance_variables



738
739
740
741
742
743
744
745
746
747
# File 'lib/jss/api_connection.rb', line 738

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.delete :@ext_attr_definition_cache
  vars
end

#put_rsrc(rsrc, xml) ⇒ String

Update an existing JSS resource

Parameters:

  • rsrc (String)

    the API resource being changed, the URL part after 'JSSResource/'

  • xml (String)

    the xml specifying the changes.

Returns:

  • (String)

    the xml response from the server.



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/jss/api_connection.rb', line 556

def put_rsrc(rsrc, xml)
  validate_connected

  # convert CRs & to &#13;
  xml.gsub!(/\r/, '&#13;')

  # send the data
  @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
end

#timeout=(timeout) ⇒ void

This method returns an undefined value.

Reset the response timeout for the rest connection

Parameters:

  • timeout (Integer)

    the new timeout in seconds



478
479
480
# File 'lib/jss/api_connection.rb', line 478

def timeout=(timeout)
  @cnx.options[:timeout] = timeout
end

#to_sString

A useful string about this connection

Returns:



468
469
470
# File 'lib/jss/api_connection.rb', line 468

def to_s
  @connected ? "Using #{@rest_url} as user #{@user}" : 'not connected'
end

#upload(rsrc, local_file) ⇒ String

Upload a file. This is really only used for the 'fileuploads' endpoint, as implemented in the Uploadable mixin module, q.v.

Parameters:

  • rsrc (String)

    the API resource being uploadad-to, the URL part after 'JSSResource/'

  • local_file (String, Pathname)

    the local file to upload

Returns:

  • (String)

    the xml response from the server.



616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'lib/jss/api_connection.rb', line 616

def upload(rsrc, local_file)
  validate_connected

  # the upload file object for faraday
  local_file = Pathname.new local_file
  upfile = Faraday::UploadIO.new(
    local_file.to_s,
    'application/octet-stream',
    local_file.basename.to_s
  )

  # send it and get the response
  @last_http_response =
    @cnx.post rsrc do |req|
      req.headers['Content-Type'] = 'multipart/form-data'
      req.body = { name: upfile }
    end

  unless @last_http_response.success?
    handle_http_error
    return false
  end

  true
end

#valid_server?(server, port = SSL_PORT) ⇒ Boolean

Test that a given hostname & port is a JSS API server

Parameters:

  • server (String)

    The hostname to test,

  • port (Integer) (defaults to: SSL_PORT)

    The port to try connecting on

Returns:

  • (Boolean)

    does the server host a JSS API?



675
676
677
678
679
680
681
682
# File 'lib/jss/api_connection.rb', line 675

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
end