ruby-jss 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ruby-jss might be problematic. Click here for more details.

Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +7 -0
  3. data/CHANGES.md +112 -0
  4. data/LICENSE.txt +174 -0
  5. data/README.md +426 -0
  6. data/THANKS.md +6 -0
  7. data/bin/cgrouper +485 -0
  8. data/bin/subnet-update +400 -0
  9. data/lib/jss-api.rb +2 -0
  10. data/lib/jss.rb +190 -0
  11. data/lib/jss/api_connection.rb +410 -0
  12. data/lib/jss/api_object.rb +616 -0
  13. data/lib/jss/api_object/advanced_search.rb +389 -0
  14. data/lib/jss/api_object/advanced_search/advanced_computer_search.rb +95 -0
  15. data/lib/jss/api_object/advanced_search/advanced_mobile_device_search.rb +96 -0
  16. data/lib/jss/api_object/advanced_search/advanced_user_search.rb +95 -0
  17. data/lib/jss/api_object/building.rb +92 -0
  18. data/lib/jss/api_object/category.rb +147 -0
  19. data/lib/jss/api_object/computer.rb +852 -0
  20. data/lib/jss/api_object/creatable.rb +98 -0
  21. data/lib/jss/api_object/criteriable.rb +189 -0
  22. data/lib/jss/api_object/criteriable/criteria.rb +231 -0
  23. data/lib/jss/api_object/criteriable/criterion.rb +228 -0
  24. data/lib/jss/api_object/department.rb +93 -0
  25. data/lib/jss/api_object/distribution_point.rb +560 -0
  26. data/lib/jss/api_object/extendable.rb +221 -0
  27. data/lib/jss/api_object/extension_attribute.rb +466 -0
  28. data/lib/jss/api_object/extension_attribute/computer_extension_attribute.rb +362 -0
  29. data/lib/jss/api_object/extension_attribute/mobile_device_extension_attribute.rb +189 -0
  30. data/lib/jss/api_object/extension_attribute/user_extension_attribute.rb +117 -0
  31. data/lib/jss/api_object/group.rb +380 -0
  32. data/lib/jss/api_object/group/computer_group.rb +124 -0
  33. data/lib/jss/api_object/group/mobile_device_group.rb +139 -0
  34. data/lib/jss/api_object/group/user_group.rb +139 -0
  35. data/lib/jss/api_object/ldap_server.rb +535 -0
  36. data/lib/jss/api_object/locatable.rb +286 -0
  37. data/lib/jss/api_object/matchable.rb +97 -0
  38. data/lib/jss/api_object/mobile_device.rb +556 -0
  39. data/lib/jss/api_object/netboot_server.rb +148 -0
  40. data/lib/jss/api_object/network_segment.rb +414 -0
  41. data/lib/jss/api_object/osx_configuration_profile.rb +262 -0
  42. data/lib/jss/api_object/package.rb +839 -0
  43. data/lib/jss/api_object/peripheral.rb +335 -0
  44. data/lib/jss/api_object/peripheral_type.rb +295 -0
  45. data/lib/jss/api_object/policy.rb +898 -0
  46. data/lib/jss/api_object/purchasable.rb +316 -0
  47. data/lib/jss/api_object/removable_macaddr.rb +98 -0
  48. data/lib/jss/api_object/scopable.rb +136 -0
  49. data/lib/jss/api_object/scopable/scope.rb +621 -0
  50. data/lib/jss/api_object/script.rb +631 -0
  51. data/lib/jss/api_object/self_servable.rb +356 -0
  52. data/lib/jss/api_object/site.rb +93 -0
  53. data/lib/jss/api_object/software_update_server.rb +109 -0
  54. data/lib/jss/api_object/updatable.rb +117 -0
  55. data/lib/jss/api_object/uploadable.rb +138 -0
  56. data/lib/jss/api_object/user.rb +272 -0
  57. data/lib/jss/client.rb +504 -0
  58. data/lib/jss/compatibility.rb +66 -0
  59. data/lib/jss/composer.rb +185 -0
  60. data/lib/jss/configuration.rb +306 -0
  61. data/lib/jss/db_connection.rb +298 -0
  62. data/lib/jss/exceptions.rb +95 -0
  63. data/lib/jss/ruby_extensions.rb +35 -0
  64. data/lib/jss/ruby_extensions/filetest.rb +43 -0
  65. data/lib/jss/ruby_extensions/hash.rb +79 -0
  66. data/lib/jss/ruby_extensions/ipaddr.rb +91 -0
  67. data/lib/jss/ruby_extensions/pathname.rb +77 -0
  68. data/lib/jss/ruby_extensions/string.rb +59 -0
  69. data/lib/jss/ruby_extensions/time.rb +63 -0
  70. data/lib/jss/server.rb +108 -0
  71. data/lib/jss/utility.rb +478 -0
  72. data/lib/jss/version.rb +31 -0
  73. metadata +187 -0
@@ -0,0 +1,410 @@
1
+ ### Copyright 2016 Pixar
2
+ ###
3
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
4
+ ### with the following modification; you may not use this file except in
5
+ ### compliance with the Apache License and the following modification to it:
6
+ ### Section 6. Trademarks. is deleted and replaced with:
7
+ ###
8
+ ### 6. Trademarks. This License does not grant permission to use the trade
9
+ ### names, trademarks, service marks, or product names of the Licensor
10
+ ### and its affiliates, except as required to comply with Section 4(c) of
11
+ ### the License and to reproduce the content of the NOTICE file.
12
+ ###
13
+ ### You may obtain a copy of the Apache License at
14
+ ###
15
+ ### http://www.apache.org/licenses/LICENSE-2.0
16
+ ###
17
+ ### Unless required by applicable law or agreed to in writing, software
18
+ ### distributed under the Apache License with the above modification is
19
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20
+ ### KIND, either express or implied. See the Apache License for the specific
21
+ ### language governing permissions and limitations under the Apache License.
22
+ ###
23
+ ###
24
+
25
+ ###
26
+ module JSS
27
+
28
+ #####################################
29
+ ### Constants
30
+ #####################################
31
+
32
+ #####################################
33
+ ### Module Variables
34
+ #####################################
35
+
36
+ #####################################
37
+ ### Module Methods
38
+ #####################################
39
+
40
+ #####################################
41
+ ### Module Classes
42
+ #####################################
43
+
44
+ ###
45
+ ### An API connection to the JSS.
46
+ ###
47
+ ### This is a singleton class, only one can exist at a time.
48
+ ### Its one instance is created automatically when the module loads, but it
49
+ ### isn't connected to anything at that time.
50
+ ###
51
+ ### Use it via the {JSS::API} constant to call the #connect
52
+ ### method, and the {#get_rsrc}, {#put_rsrc}, {#post_rsrc}, & {#delete_rsrc}
53
+ ### methods, q.v. below.
54
+ ###
55
+ ### To access the underlying RestClient::Resource instance,
56
+ ### use JSS::API.cnx
57
+ ###
58
+ class APIConnection
59
+ include Singleton
60
+
61
+ #####################################
62
+ ### Class Constants
63
+ #####################################
64
+
65
+ ### The base API path in the jss URL
66
+ RSRC_BASE = "JSSResource"
67
+
68
+ ### A url path to load to see if there's an API available at a host.
69
+ ### This just loads the API resource docs page
70
+ TEST_PATH = "#{RSRC_BASE}/accounts"
71
+
72
+ ### If the test path loads correctly from a casper server, it'll contain
73
+ ### this text (this is what we get when we make an unauthenticated
74
+ ### API call.)
75
+ TEST_CONTENT = "<p>The request requires user authentication</p>"
76
+
77
+ ### The Default port
78
+ HTTP_PORT = 9006
79
+
80
+ ### The SSL port
81
+ SSL_PORT = 8443
82
+
83
+ ### The top line of an XML doc for submitting data via API
84
+ XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
85
+
86
+ ### Default timeouts in seconds
87
+ DFT_OPEN_TIMEOUT = 60
88
+ DFT_TIMEOUT = 60
89
+
90
+ ### The Default SSL Version
91
+ DFT_SSL_VERSION = 'TLSv1'
92
+
93
+ #####################################
94
+ ### Attributes
95
+ #####################################
96
+
97
+ ### @return [String] the username who's connected to the JSS API
98
+ attr_reader :jss_user
99
+
100
+ ### @return [RestClient::Resource] the underlying connection resource
101
+ attr_reader :cnx
102
+
103
+ ### @return [Boolean] are we connected right now?
104
+ attr_reader :connected
105
+
106
+ ### @return [JSS::Server] the details of the JSS to which we're connected.
107
+ attr_reader :server
108
+
109
+ ### @return [String] the hostname of the JSS to which we're connected.
110
+ attr_reader :server_host
111
+
112
+ #####################################
113
+ ### Constructor
114
+ #####################################
115
+
116
+ ###
117
+ ### To connect, use JSS::APIConnection.instance.connect
118
+ ### or a shortcut, JSS::API.connect
119
+ ###
120
+ def initialize ()
121
+ @connected = false
122
+ end # init
123
+
124
+ #####################################
125
+ ### Class Methods
126
+ #####################################
127
+
128
+ ###
129
+ ### Connect to the JSS API.
130
+ ###
131
+ ### @param args[Hash] the keyed arguments for connection.
132
+ ###
133
+ ### @option args :server[String] the hostname of the JSS API server, required if not defined in JSS::CONFIG
134
+ ###
135
+ ### @option args :port[Integer] the port number to connect with, defaults to 8443
136
+ ###
137
+ ### @option args :use_ssl[Boolean] should the connection be made over SSL? Defaults to true.
138
+ ###
139
+ ### @option args :verify_cert[Boolean] should HTTPS SSL certificates be verified. Defaults to true.
140
+ ### If your connection raises RestClient::SSLCertificateNotVerified, and you don't care about the
141
+ ### validity of the SSL cert. just set this explicitly to false.
142
+ ###
143
+ ### @option args :user[String] a JSS user who has API privs, required if not defined in JSS::CONFIG
144
+ ###
145
+ ### @option args :pw[String,Symbol] Required, the password for that user, or :prompt, or :stdin
146
+ ### If :prompt, the user is promted on the commandline to enter the password for the :user.
147
+ ### If :stdin#, the password is read from a line of std in represented by the digit at #,
148
+ ### so :stdin3 reads the passwd from the third line of standard input. defaults to line 1,
149
+ ### if no digit is supplied. see {JSS.stdin}
150
+ ###
151
+ ### @option args :open_timeout[Integer] the number of seconds to wait for an initial response, defaults to 60
152
+ ###
153
+ ### @option args :timeout[Integer] the number of seconds before an API call times out, defaults to 60
154
+ ###
155
+ ### @return [true]
156
+ ###
157
+ def connect (args = {})
158
+
159
+ # the server, if not specified, might come from a couple places.
160
+ # see #hostname
161
+ args[:server] ||= hostname
162
+
163
+ # settings from config if they aren't in the args
164
+ args[:server] ||= JSS::CONFIG.api_server_name
165
+ args[:port] ||= JSS::CONFIG.api_server_port
166
+ args[:user] ||= JSS::CONFIG.api_username
167
+ args[:timeout] ||= JSS::CONFIG.api_timeout
168
+ args[:open_timeout] ||= JSS::CONFIG.api_timeout_open
169
+ args[:ssl_version] ||= JSS::CONFIG.api_ssl_version
170
+
171
+ # if verify cert given was NOT in the args....
172
+ if args[:verify_cert].nil?
173
+ # set it from the prefs
174
+ args[:verify_cert] = JSS::CONFIG.api_verify_cert
175
+ end
176
+
177
+ # settings from client jamf plist if needed
178
+ args[:port] ||= JSS::Client.jss_port
179
+
180
+ # default settings if needed
181
+ args[:port] ||= SSL_PORT
182
+ args[:timeout] ||= DFT_TIMEOUT
183
+ args[:open_timeout] ||= DFT_OPEN_TIMEOUT
184
+
185
+ # As of Casper 9.61 we can't use SSL, must use TLS, since SSLv3 was susceptible to poodles.
186
+ # NOTE - this requires rest-client v 1.7.0 or higher
187
+ # which requires mime-types 2.0 or higher, which requires ruby 1.9.2 or higher!
188
+ # That means that support for ruby 1.8.7 stops with Casper 9.6
189
+ args[:ssl_version] ||= DFT_SSL_VERSION
190
+
191
+
192
+ # must have server, user, and pw
193
+ raise JSS::MissingDataError, "No JSS :server specified, or in configuration." unless args[:server]
194
+ raise JSS::MissingDataError, "No JSS :user specified, or in configuration." unless args[:user]
195
+ raise JSS::MissingDataError, "Missing :pw for user '#{args[:user]}'" unless args[:pw]
196
+
197
+ # we're using ssl if 1) args[:use_ssl] is anything but false
198
+ # or 2) the port is the default casper ssl port.
199
+ args[:use_ssl] = (not args[:use_ssl] == false) or (args[:port] == SSL_PORT)
200
+
201
+ # and here's the URL
202
+ ssl = args[:use_ssl] ? "s" : ''
203
+ @rest_url = URI::encode "http#{ssl}://#{args[:server]}:#{args[:port]}/#{RSRC_BASE}"
204
+
205
+
206
+ # prep the args for passing to RestClient::Resource
207
+ # if verify_cert is anything but false, we will verify
208
+ args[:verify_ssl] = (args[:verify_cert] == false) ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
209
+
210
+ args[:password] = if args[:pw] == :prompt
211
+ JSS.prompt_for_password "Enter the password for JSS user #{args[:user]}@#{args[:server]}:"
212
+ elsif args[:pw].is_a?(Symbol) and args[:pw].to_s.start_with?('stdin')
213
+ args[:pw].to_s =~ /^stdin(\d+)$/
214
+ line = $1
215
+ line ||= 1
216
+ JSS.stdin line
217
+ else
218
+ args[:pw]
219
+ end
220
+
221
+ # heres our connection
222
+ @cnx = RestClient::Resource.new("#{@rest_url}", args)
223
+
224
+ @jss_user = args[:user]
225
+ @server_host = args[:server]
226
+ @connected = true
227
+ @server = JSS::Server.new
228
+
229
+ if @server.version < JSS.parse_jss_version(JSS::MINIMUM_SERVER_VERSION)[:version]
230
+ raise JSS::UnsupportedError, "Your JSS Server version, #{@server.raw_version}, is to low. Must be #{JSS::MINIMUM_SERVER_VERSION} or higher."
231
+ end
232
+
233
+ return @connected ? @server_host : nil
234
+ end # connect
235
+
236
+ ###
237
+ ### Reset the response timeout for the rest connection
238
+ ###
239
+ ### @param timeout[Integer] the new timeout in seconds
240
+ ###
241
+ ### @return [void]
242
+ ###
243
+ def timeout= (timeout)
244
+ @cnx.options[:timeout] = timeout
245
+ end
246
+
247
+ ###
248
+ ### Reset the open-connection timeout for the rest connection
249
+ ###
250
+ ### @param timeout[Integer] the new timeout in seconds
251
+ ###
252
+ ### @return [void]
253
+ ###
254
+ def open_timeout= (timeout)
255
+ @cnx.options[:open_timeout] = timeout
256
+ end
257
+
258
+
259
+ ###
260
+ ### With a REST connection, there isn't any real "connection" to disconnect from
261
+ ### So to disconnect, we just unset all our credentials.
262
+ ###
263
+ ### @return [void]
264
+ ###
265
+ def disconnect
266
+ @jss_user = nil
267
+ @rest_url = nil
268
+ @server_host = nil
269
+ @cnx = nil
270
+ @connected = false
271
+ end # disconnect
272
+
273
+ ###
274
+ ### Get an arbitrary JSS resource
275
+ ###
276
+ ### The first argument is the resource to get (the part of the API url
277
+ ### after the 'JSSResource/' )
278
+ ###
279
+ ### By default we get the data in JSON, and parse it
280
+ ### into a ruby data structure (arrays, hashes, strings, etc)
281
+ ### with symbolized Hash keys.
282
+ ###
283
+ ### @param rsrc[String] the resource to get
284
+ ### (the part of the API url after the 'JSSResource/' )
285
+ ###
286
+ ### @param format[Symbol] either ;json or :xml
287
+ ### If the second argument is :xml, the XML data is returned as a String.
288
+ ###
289
+ ### @return [Hash,String] the result of the get
290
+ ###
291
+ def get_rsrc (rsrc, format = :json)
292
+ raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
293
+ rsrc = URI::encode rsrc
294
+ data = @cnx[rsrc].get(:accept => format)
295
+ return JSON.parse(data, :symbolize_names => true) if format == :json
296
+ data
297
+ end
298
+
299
+ ###
300
+ ### Change an existing JSS resource
301
+ ###
302
+ ### @param rsrc[String] the API resource being changed, the URL part after 'JSSResource/'
303
+ ###
304
+ ### @param xml[String] the xml specifying the changes.
305
+ ###
306
+ ### @return [String] the xml response from the server.
307
+ ###
308
+ def put_rsrc(rsrc,xml)
309
+ raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
310
+
311
+ ### convert CRs & to &#13;
312
+ xml.gsub!(/\r/, '&#13;')
313
+
314
+ ### send the data
315
+ @cnx[rsrc].put(xml, :content_type => 'text/xml')
316
+ end
317
+
318
+ ###
319
+ ### Create a new JSS resource
320
+ ###
321
+ ### @param rsrc[String] the API resource being created, the URL part after 'JSSResource/'
322
+ ###
323
+ ### @param xml[String] the xml specifying the new object.
324
+ ###
325
+ ### @return [String] the xml response from the server.
326
+ ###
327
+ def post_rsrc(rsrc,xml)
328
+ raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
329
+
330
+ ### convert CRs & to &#13;
331
+ xml.gsub!(/\r/, '&#13;')
332
+
333
+ ### send the data
334
+ @cnx[rsrc].post xml, :content_type => 'text/xml', :accept => :json
335
+ end #post_rsrc
336
+
337
+ ### Delete a resource from the JSS
338
+ ###
339
+ ### @param rsrc[String] the resource to create, the URL part after 'JSSResource/'
340
+ ###
341
+ ### @return [String] the xml response from the server.
342
+ ###
343
+ def delete_rsrc(rsrc)
344
+ raise JSS::InvalidConnectionError, "Not Connected. Use JSS::API.connect first." unless @connected
345
+ raise MissingDataError, "Missing :rsrc" if rsrc.nil?
346
+
347
+ ### delete the resource
348
+ @cnx[rsrc].delete
349
+
350
+ end #delete_rsrc
351
+
352
+
353
+ ### Test that a given hostname & port is a JSS API server
354
+ ###
355
+ ### @param server[String] The hostname to test,
356
+ ###
357
+ ### @param port[Integer] The port to try connecting on
358
+ ###
359
+ ### @return [Boolean] does the server host a JSS API?
360
+ ###
361
+ def valid_server? (server, port = SSL_PORT)
362
+ # cheating by shelling out to curl, because getting open-uri, or even net/http to use
363
+ # ssl_options like :OP_NO_SSLv2 and :OP_NO_SSLv3 will take time to figure out..
364
+ return true if `/usr/bin/curl -s 'https://#{server}:#{port}/#{TEST_PATH}'`.include? TEST_CONTENT
365
+ return true if `/usr/bin/curl -s 'http://#{server}:#{port}/#{TEST_PATH}'`.include? TEST_CONTENT
366
+ return false
367
+
368
+ # try ssl first
369
+ # NOTE: doesn't work if we can't disallow SSLv3 or force TLSv1
370
+ # See cheat above.
371
+ begin
372
+ return true if open("https://#{server}:#{port}/#{TEST_PATH}", ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE).read.include? TEST_CONTENT
373
+
374
+ rescue
375
+ # then regular http
376
+ begin
377
+ return true if open("http://#{server}:#{port}/#{TEST_PATH}").read.include? TEST_CONTENT
378
+ rescue
379
+ # any errors = no API
380
+ return false
381
+ end # begin
382
+ end #begin
383
+ # if we're here, no API
384
+ return false
385
+ end
386
+
387
+ ### The server to which we are connected, or will
388
+ ### try connecting to if none is specified with the
389
+ ### call to #connect
390
+ ###
391
+ ### @return [String] the hostname of the server
392
+ ###
393
+ def hostname
394
+ return @server_host if @server_host
395
+ srvr = JSS::CONFIG.api_server_name
396
+ srvr ||= JSS::Client.jss_server
397
+ return srvr
398
+ end
399
+
400
+ ### aliases
401
+ alias connected? connected
402
+
403
+
404
+ end # class JSSAPIConnection
405
+
406
+ ### The single instance of the APIConnection
407
+ API = APIConnection.instance
408
+
409
+
410
+ end # module
@@ -0,0 +1,616 @@
1
+ ### Copyright 2016 Pixar
2
+ ###
3
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
4
+ ### with the following modification; you may not use this file except in
5
+ ### compliance with the Apache License and the following modification to it:
6
+ ### Section 6. Trademarks. is deleted and replaced with:
7
+ ###
8
+ ### 6. Trademarks. This License does not grant permission to use the trade
9
+ ### names, trademarks, service marks, or product names of the Licensor
10
+ ### and its affiliates, except as required to comply with Section 4(c) of
11
+ ### the License and to reproduce the content of the NOTICE file.
12
+ ###
13
+ ### You may obtain a copy of the Apache License at
14
+ ###
15
+ ### http://www.apache.org/licenses/LICENSE-2.0
16
+ ###
17
+ ### Unless required by applicable law or agreed to in writing, software
18
+ ### distributed under the Apache License with the above modification is
19
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20
+ ### KIND, either express or implied. See the Apache License for the specific
21
+ ### language governing permissions and limitations under the Apache License.
22
+ ###
23
+ ###
24
+
25
+ ###
26
+ module JSS
27
+
28
+ #####################################
29
+ ### Module Variables
30
+ #####################################
31
+
32
+ #####################################
33
+ ### Module Methods
34
+ #####################################
35
+
36
+ #####################################
37
+ ### Classes
38
+ #####################################
39
+
40
+ ###
41
+ ### This class is the parent to all JSS API objects. It provides standard methods and structures
42
+ ### that apply to all API resouces.
43
+ ###
44
+ ### See the README.md file for general info about using subclasses of JSS::APIObject
45
+ ###
46
+ ### == Subclassing
47
+ ###
48
+ ### === Constructor
49
+ ###
50
+ ### In general, subclasses should do any class-specific argument checking before
51
+ ### calling super, and then afterwards, use the contents of @init_data to populate
52
+ ### any class-specific attributes. @id, @name, @rest_rsrc, and @in_jss are handled here.
53
+ ###
54
+ ### If a subclass can be looked up by some key other than :name or :id, the subclass must
55
+ ### pass the keys as an Array in the second argument when calling super from #initialize.
56
+ ### See {JSS::Computer#initialize} for an example of how to implement this feature.
57
+ ###
58
+ ### === Object Creation
59
+ ###
60
+ ### If a subclass should be able to be created in the JSS be sure to include {JSS::Creatable}
61
+ ###
62
+ ### The constructor should verify any extra required data (aside from :name) in the args before or after
63
+ ### calling super.
64
+ ###
65
+ ### See {JSS::Creatable} for more details.
66
+ ###
67
+ ### === Object Modification
68
+ ###
69
+ ### If a subclass should be modifiable in the JSS, include {JSS::Updatable}, q.v. for details.
70
+ ###
71
+ ### === Object Deletion
72
+ ###
73
+ ### All subclasses can be deleted in the JSS.
74
+ ###
75
+ ### === Required Constants
76
+ ###
77
+ ### Subclasses *must* provide certain Constants in order to correctly interpret API data and
78
+ ### communicate with the API.
79
+ ###
80
+ ### ==== RSRC_BASE = [String], The base for REST resources of this class
81
+ ###
82
+ ### e.g. 'computergroups' in "https://casper.mycompany.com:8443/JSSResource/computergroups/id/12"
83
+ ###
84
+ ### ==== RSRC_LIST_KEY = [Symbol] The Hash key for the JSON list output of all objects of this class in the JSS.
85
+ ###
86
+ ### e.g. the JSON output of resource "JSSResource/computergroups" is a hash
87
+ ### with one item (an Array of computergroups). That item's key is the Symbol :computer_groups
88
+ ###
89
+ ### ==== RSRC_OBJECT_KEY = [Symbol] The Hash key used for individual JSON object output.
90
+ ### It's also used in various error messages
91
+ ###
92
+ ### e.g. the JSON output of the resource "JSSResource/computergroups/id/436" is
93
+ ### a hash with one item (another hash with details of one computergroup).
94
+ ### That item's key is the Symbol :computer_group
95
+ ###
96
+ ### ==== VALID_DATA_KEYS = [Array<Symbol>] The Hash keys used to verify validity of :data
97
+ ### When instantiating a subclass using :data => somehash, some minimal checks are performed
98
+ ### to ensure the data is valid for the subclass
99
+ ###
100
+ ### The Symbols in this Array are compared to the keys of the hash provided.
101
+ ### If any of these don't exist in the hash's keys, then the :data is
102
+ ### not valid and an exception is raised.
103
+ ###
104
+ ### The keys :id and :name must always exist in the hash.
105
+ ### If only :id and :name are valid, VALID_DATA_KEYS should be an empty array.
106
+ ###
107
+ ### e.g. for a department, only :id and :name are valid, so VALID_DATA_KEYS is an empty Array ([])
108
+ ### but for a computer group, the keys :computers and :is_smart must be present as well.
109
+ ### so VALID_DATA_KEYS will be [:computers, :is_smart]
110
+ ###
111
+ ### *NOTE* Some API objects have data broken into subsections, in which case the
112
+ ### VALID_DATA_KEYS are expected in the section :general.
113
+ ###
114
+ class APIObject
115
+
116
+ #####################################
117
+ ### Mix-Ins
118
+ #####################################
119
+
120
+ #####################################
121
+ ### Class Variables
122
+ #####################################
123
+
124
+ ### This Hash holds the most recent API query for a list of all items in any subclass,
125
+ ### keyed by the subclass's RSRC_LIST_KEY. See the self.all class method.
126
+ ###
127
+ ### When the .all method is called without an argument, and this hash has
128
+ ### a matching value, the value is returned, rather than requerying the
129
+ ### API. The first time a class calls .all, or whnever refresh is
130
+ ### not false, the API is queried and the value in this hash is updated.
131
+ ###
132
+ @@all_items = {}
133
+
134
+ #####################################
135
+ ### Class Methods
136
+ #####################################
137
+
138
+ ###
139
+ ### Return an Array of Hashes for all objects of this subclass in the JSS.
140
+ ###
141
+ ### This method is only valid in subclasses of JSS::APIObject, and is
142
+ ### the parsed JSON output of an API query for the resource defined in the subclass's RSRC_BASE,
143
+ ### e.g. for JSS::Computer, with the RSRC_BASE of :computers,
144
+ ### This method retuens the output of the 'JSSResource/computers' resource,
145
+ ### which is a list of all computers in the JSS.
146
+ ###
147
+ ### Each item in the Array is a Hash with at least two keys, :id and :name.
148
+ ### The class methods .all_ids and .all_names provide easier access to those data
149
+ ### as mapped Arrays.
150
+ ###
151
+ ### Some API classes provide other data in each Hash, e.g. :udid (for computers
152
+ ### and mobile devices) or :is_smart (for groups).
153
+ ###
154
+ ### Subclasses implementing those API classes should provide .all_xxx
155
+ ### class methods for accessing those other values as mapped Arrays,
156
+ ### e.g. JSS::Computer.all_udids
157
+ ###
158
+ ### The results of the first query for each subclass is stored in @@all_items
159
+ ### and returned at every future call, so as to not requery the server every time.
160
+ ###
161
+ ### To force requerying to get updated data, provided a non-false argument.
162
+ ### I usually use :refresh, so that it's obvious what I'm doing, but true, 1,
163
+ ### or anything besides false or nil will work.
164
+ ###
165
+ ### @param refresh[Boolean] should the data be re-queried from the API?
166
+ ###
167
+ ### @return [Array<Hash{:name=>String, :id=> Integer}>]
168
+ ###
169
+ def self.all(refresh = false)
170
+ raise JSS::UnsupportedError, ".all can only be called on subclasses of JSS::APIObject" if self == JSS::APIObject
171
+ @@all_items[self::RSRC_LIST_KEY] = nil if refresh
172
+ return @@all_items[self::RSRC_LIST_KEY] if @@all_items[self::RSRC_LIST_KEY]
173
+ @@all_items[self::RSRC_LIST_KEY] = JSS::API.get_rsrc(self::RSRC_BASE)[self::RSRC_LIST_KEY]
174
+ end
175
+
176
+ ###
177
+ ### Returns an Array of the JSS id numbers of all the members
178
+ ### of the subclass.
179
+ ###
180
+ ### e.g. When called from subclass JSS::Computer,
181
+ ### returns the id's of all computers in the JSS
182
+ ###
183
+ ### @param refresh[Boolean] should the data be re-queried from the API?
184
+ ###
185
+ ### @return [Array<Integer>] the ids of all items of this subclass in the JSS
186
+ ###
187
+ def self.all_ids(refresh = false)
188
+ self.all(refresh).map{|i| i[:id]}
189
+ end
190
+
191
+ ###
192
+ ### Returns an Array of the JSS names of all the members
193
+ ### of the subclass.
194
+ ###
195
+ ### e.g. When called from subclass JSS::Computer,
196
+ ### returns the names of all computers in the JSS
197
+ ###
198
+ ### @param refresh[Boolean] should the data be re-queried from the API?
199
+ ###
200
+ ### @return [Array<String>] the names of all item of this subclass in the JSS
201
+ ###
202
+ def self.all_names(refresh = false)
203
+ self.all(refresh).map{|i| i[:name]}
204
+ end
205
+
206
+ ###
207
+ ### Return a hash of all objects of this subclass
208
+ ### in the JSS where the key is the id, and the value
209
+ ### is some other key in the data items returned by the JSS::APIObject.all.
210
+ ###
211
+ ### If the other key doesn't exist in the API
212
+ ### data, (eg :udid for JSS::Department) the values will be nil.
213
+ ###
214
+ ### Use this method to map ID numbers to other identifiers returned
215
+ ### by the API list resources. Invert its result to map the other
216
+ ### identfier to ids.
217
+ ###
218
+ ### @example
219
+ ### JSS::Computer.map_all_ids_to(:name)
220
+ ###
221
+ ### # Returns, eg {2 => "kimchi", 5 => "mantis"}
222
+ ###
223
+ ### JSS::Computer.map_all_ids_to(:name).invert
224
+ ###
225
+ ### # Returns, eg {"kimchi" => 2, "mantis" => 5}
226
+ ###
227
+ ### @param other_key[Symbol] the other data key with which to associate each id
228
+ ###
229
+ ### @param refresh[Boolean] should the data re-queried from the API?
230
+ ###
231
+ ### @return [Hash{Integer => Oject}] the associated ids and data
232
+ ###
233
+ def self.map_all_ids_to(other_key, refresh = false)
234
+ h = {}
235
+ self.all(refresh).each{|i| h[i[:id]] = i[other_key]}
236
+ h
237
+ end
238
+
239
+
240
+ ### Return an Array of JSS::APIObject subclass instances
241
+ ### e.g when called on JSS::Package, return all JSS::Package
242
+ ### objects in the JSS.
243
+ ###
244
+ ### NOTE: This may be slow as it has to look up each object individually!
245
+ ### use it wisely.
246
+ ###
247
+ ### @param refresh[Boolean] should the data re-queried from the API?
248
+ ###
249
+ ### @return [Hash{Integer => Object}] the objects requested
250
+ def self.all_objects(refresh = false)
251
+ objects_key = "#{self::RSRC_LIST_KEY}_objects".to_sym
252
+ @@all_items[objects_key] = nil if refresh
253
+ return @@all_items[objects_key] if @@all_items[objects_key]
254
+ @@all_items[objects_key] = self.all(refresh = false).map{|o| self.new :id => o[:id]}
255
+ end
256
+
257
+
258
+
259
+ ###
260
+ ### Convert an Array of Hashes of API object data to a
261
+ ### REXML element.
262
+ ###
263
+ ### Given an Array of Hashes of items in the subclass
264
+ ### where each Hash has at least an :id or a :name key,
265
+ ### (as what comes from the .all class method)
266
+ ### return a REXML <classes> element
267
+ ### with one <class> element per Hash member.
268
+ ###
269
+ ### @example
270
+ ### # for class JSS::Computer
271
+ ### some_comps = [{:id=>2, :name=>"kimchi"},{:id=>5, :name=>"mantis"}]
272
+ ### xml_names = JSS::Computer.xml_list some_comps
273
+ ### puts xml_names # output manually formatted for clarity, xml.to_s has no newlines between elements
274
+ ###
275
+ ### <computers>
276
+ ### <computer>
277
+ ### <name>kimchi</name>
278
+ ### </computer>
279
+ ### <computer>
280
+ ### <name>mantis</name>
281
+ ### </computer>
282
+ ### </computers>
283
+ ###
284
+ ### xml_ids = JSS::Computer.xml_list some_comps, :id
285
+ ### puts xml_names # output manually formatted for clarity, xml.to_s has no newlines between elements
286
+ ###
287
+ ### <computers>
288
+ ### <computer>
289
+ ### <id>2</id>
290
+ ### </computer>
291
+ ### <computer>
292
+ ### <id>5</id>
293
+ ### </computer>
294
+ ### </computers>
295
+ ###
296
+ ### @param array[Array<Hash{:name=>String, :id =>Integer, Symbol=>#to_s}>] the Array of subclass data to convert
297
+ ###
298
+ ### @param content[Symbol] the Hash key to use as the inner element for each member of the Array
299
+ ###
300
+ ### @return [REXML::Element] the XML element representing the data
301
+ ###
302
+ def self.xml_list(array, content = :name)
303
+ JSS.item_list_to_rexml_list self::RSRC_LIST_KEY, self::RSRC_OBJECT_KEY, array, content
304
+ end
305
+
306
+ ###
307
+ ### Some API objects contain references to other API objects. Usually those
308
+ ### references are a Hash containing the :id and :name of the target. Sometimes,
309
+ ### however the reference is just the name of the target.
310
+ ###
311
+ ### A Script has a property :category, which comes from the API as
312
+ ### a String, the name of the category for that script. e.g. "GoodStuff"
313
+ ###
314
+ ### A Policy also has a property :category, but it comes from the API as a Hash
315
+ ### with both the name and id, e.g. !{:id => 8, :name => "GoodStuff"}
316
+ ###
317
+ ### When that reference is to a single thing (like the category to which something belongs)
318
+ ### APIObject subclasses usually store only the name, and use the name when
319
+ ### returning data to the API.
320
+ ###
321
+ ### When an object references a list of related objects
322
+ ### (like the computers assigned to a user) that list will be and Array of Hashes
323
+ ### as above, with both the :id and :name
324
+ ###
325
+ ###
326
+ ### This method is just a handy way to extract the name regardless of how it comes
327
+ ### from the API. Most APIObject subclasses use it in their #initialize method
328
+ ###
329
+ ### @param a_thing[String,Array] the api data from which we're extracting the name
330
+ ###
331
+ ### @return [String] the name extracted from a_thing
332
+ ###
333
+ def self.get_name(a_thing)
334
+ case a_thing
335
+ when String
336
+ a_thing
337
+ when Hash
338
+ a_thing[:name]
339
+ when nil
340
+ nil
341
+ end
342
+ end
343
+
344
+ #####################################
345
+ ### Class Constants
346
+ #####################################
347
+
348
+ ###
349
+ ### These Symbols are added to VALID_DATA_KEYS for performing the
350
+ ### :data validity test described above.
351
+ ###
352
+ REQUIRED_DATA_KEYS = [:id, :name]
353
+
354
+ ###
355
+ ### By default, these keys are available for object lookups
356
+ ### Others can be added by subclasses using an array of them
357
+ ### as the second argument to super(initialize)
358
+ ### The keys must be Symbols that match the keyname in the resource url.
359
+ ### e.g. :serialnumber for JSSResource/computers/serialnumber/xxxxx
360
+ ###
361
+ DEFAULT_LOOKUP_KEYS = [:id, :name]
362
+
363
+ #####################################
364
+ ### Attributes
365
+ #####################################
366
+
367
+ ### @return [Integer] the JSS id number
368
+ attr_reader :id
369
+
370
+ ### @return [String] the name
371
+ attr_reader :name
372
+
373
+ ### @return [Boolean] is it in the JSS?
374
+ attr_reader :in_jss
375
+
376
+ ### @return [String] the Rest resource for API access (the part after "JSSResource/" )
377
+ attr_reader :rest_rsrc
378
+
379
+ #####################################
380
+ ### Constructor
381
+ #####################################
382
+
383
+ ###
384
+ ### The args hash must include :id, :name, or :data.
385
+ ### * :id or :name will be looked up via the API
386
+ ### * * if the subclass includes JSS::Creatable, :id can be :new, to create a new object in the JSS.
387
+ ### and :name is required
388
+ ### * :data must be the JSON output of a separate {JSS::APIConnection} query (a Hash of valid object data)
389
+ ###
390
+ ### Some subclasses can accept other options, by pasing their keys in a final Array
391
+ ###
392
+ ### @param args[Hash] the data for looking up, or constructing, a new object.
393
+ ###
394
+ ### @option args :id[Integer] the jss id to look up
395
+ ###
396
+ ### @option args :name[String] the name to look up
397
+ ###
398
+ ### @option args :data[Hash] the JSON output of a separate {JSS::APIConnection} query
399
+ ###
400
+ ### @param other_lookup_keys[Array<Symbol>] Hash keys other than :id and :name, by which an API
401
+ ### lookup may be performed.
402
+ ###
403
+ def initialize(args = {}, other_lookup_keys = [])
404
+
405
+ ################################
406
+ ################################
407
+ ####### Previously looked-up JSON data
408
+ if args[:data]
409
+
410
+ @init_data = args[:data]
411
+ ### Does this data come in subsets?
412
+ @got_subsets = @init_data[:general].kind_of?(Hash)
413
+
414
+ ### data must include all they keys in REQUIRED_DATA_KEYS + VALID_DATA_KEYS
415
+ ### in either the main hash keys or the :general sub-hash, if it exists
416
+ hash_to_check = @got_subsets ? @init_data[:general] : @init_data
417
+ combined_valid_keys = self.class::REQUIRED_DATA_KEYS + self.class::VALID_DATA_KEYS
418
+ keys_ok = (hash_to_check.keys & combined_valid_keys).count == combined_valid_keys.count
419
+ raise JSS::InvalidDataError, ":data is not valid JSON for a #{self.class::RSRC_OBJECT_KEY} from the API. It needs at least the keys :#{combined_valid_keys.join ', :'}" unless keys_ok
420
+
421
+ ### and the id must be in the jss
422
+ raise NoSuchItemError, "No #{self.class::RSRC_OBJECT_KEY} with JSS id: #{@init_data[:id]}" unless self.class.all_ids.include? hash_to_check[:id]
423
+
424
+
425
+ ################################
426
+ ################################
427
+ ###### Make a new one in the JSS, but only if we've included the Creatable module
428
+ elsif args[:id] == :new
429
+
430
+ raise JSS::UnsupportedError, "Creating #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless defined? self.class::CREATABLE
431
+
432
+ raise JSS::MissingDataError, "You must provide a :name for a new #{self.class::RSRC_OBJECT_KEY}." unless args[:name]
433
+
434
+ raise JSS::AlreadyExistsError, "A #{self.class::RSRC_OBJECT_KEY} already exists with the name '#{args[:name]}'" if self.class.all_names.include? args[:name]
435
+
436
+
437
+ ### NOTE: subclasses may want to pre-populate more keys in @init_data when :id == :new
438
+ ### then parse them into attributes later.
439
+ @name = args[:name]
440
+ @init_data = {:name => args[:name]}
441
+ @in_jss = false
442
+ @rest_rsrc = "#{self.class::RSRC_BASE}/name/#{URI.escape @name}"
443
+ @need_to_update = true
444
+ return
445
+
446
+ ################################
447
+ ################################
448
+ ###### Look up the data via the API
449
+ else
450
+ ### what lookup key are we using?
451
+ combined_lookup_keys = self.class::DEFAULT_LOOKUP_KEYS + other_lookup_keys
452
+ lookup_key = (combined_lookup_keys & args.keys)[0]
453
+
454
+ raise JSS::MissingDataError, "Args must include :#{combined_lookup_keys.join(', :')}, or :data" unless lookup_key
455
+
456
+ rsrc = "#{self.class::RSRC_BASE}/#{lookup_key}/#{args[lookup_key]}"
457
+
458
+ begin
459
+ @init_data = JSS::API.get_rsrc(rsrc)[self.class::RSRC_OBJECT_KEY]
460
+
461
+ ### If we're looking up by id or name and we're here,
462
+ ### then we have it regardless of which subset it's in
463
+ ### otherwise, first assume they are in the init hash
464
+ @id = lookup_key == :id ? args[lookup_key] : @init_data[:id]
465
+ @name = lookup_key == :name ? args[lookup_key] : @init_data[:name]
466
+
467
+ rescue RestClient::ResourceNotFound
468
+ raise NoSuchItemError, "No #{self.class::RSRC_OBJECT_KEY} found matching: #{args[:name] ? args[:name] : args[:id]}"
469
+ end
470
+ end ## end arg parsing
471
+
472
+ # Find the "main" subset which contains :id and :name
473
+ #
474
+ # If they aren't at the top-level of the init hash they are in a subset hash,
475
+ # usually :general, but sometimes someething else,
476
+ # like ldap servers, which have them in :connection
477
+ # Whereever both :id and :name are, that's the main subset
478
+
479
+ @init_data.keys.each do |subset|
480
+ @main_subset = @init_data[subset] if @init_data[subset].kind_of? Hash and @init_data[subset][:id] and @init_data[subset][:name]
481
+ break if @main_subset
482
+ end
483
+ @main_subset ||= @init_data
484
+
485
+ @id ||= @main_subset[:id]
486
+ @name ||= @main_subset[:name]
487
+
488
+ # many things have a :site
489
+ if @main_subset[:site]
490
+ @site = JSS::APIObject.get_name( @main_subset[:site])
491
+ end
492
+
493
+ # many things have a :category
494
+ if @main_subset[:category]
495
+ @category = JSS::APIObject.get_name( @main_subset[:category])
496
+ end
497
+
498
+ # set empty strings to nil
499
+ @init_data.jss_nillify! '', :recurse
500
+
501
+ @in_jss = true
502
+ @rest_rsrc = "#{self.class::RSRC_BASE}/id/#{@id}"
503
+ @need_to_update = false
504
+ end # init
505
+
506
+ #####################################
507
+ ### Public Instance Methods
508
+ #####################################
509
+
510
+ ###
511
+ ### Either Create or Update this object in the JSS
512
+ ###
513
+ ### If this item is creatable or updatable, then
514
+ ### create it if needed, or update it if it already exists.
515
+ ###
516
+ ### @return [Integer] the id of the item created or updated
517
+ ###
518
+ def save
519
+ if @in_jss
520
+ raise JSS::UnsupportedError, "Updating this object in the JSS is currently not supported" \
521
+ unless defined? self.class::UPDATABLE
522
+ return self.update
523
+ else
524
+ raise JSS::UnsupportedError, "Creating this object in the JSS is currently not supported" \
525
+ unless defined? self.class::CREATABLE
526
+ return self.create
527
+ end
528
+ end
529
+
530
+
531
+ ###
532
+ ### Delete this item from the JSS.
533
+ ###
534
+ ### Subclasses may want to redefine this method,
535
+ ### first calling super, then setting other attributes to
536
+ ### nil, false, empty, etc..
537
+ ###
538
+ ### @return [void]
539
+ ###
540
+ def delete
541
+ return nil unless @in_jss
542
+ JSS::API.delete_rsrc @rest_rsrc
543
+ @rest_rsrc = "#{self.class::RSRC_BASE}/name/#{URI.escape @name}"
544
+ @id = nil
545
+ @in_jss = false
546
+ @need_to_update = false
547
+ end # delete
548
+
549
+ #####################################
550
+ ### Private Instance Methods
551
+ #####################################
552
+ private
553
+
554
+ ###
555
+ ### Return a String with the XML Resource
556
+ ### for submitting creation or changes to the JSS via
557
+ ### the API via the Creatable or Updatable modules
558
+ ###
559
+ ### Most classes will redefine this method.
560
+ ###
561
+ def rest_xml
562
+ doc = REXML::Document.new APIConnection::XML_HEADER
563
+ tmpl = doc.add_element self.class::RSRC_OBJECT_KEY.to_s
564
+ tmpl.add_element('name').text = @name
565
+ return doc.to_s
566
+ end
567
+
568
+
569
+ ### Aliases
570
+
571
+ alias in_jss? in_jss
572
+
573
+ end # class APIObject
574
+
575
+
576
+
577
+ end # module JSS
578
+
579
+ ### Mix-in Sub Modules
580
+ require "jss/api_object/creatable"
581
+ require "jss/api_object/uploadable"
582
+ require "jss/api_object/locatable"
583
+ require "jss/api_object/matchable"
584
+ require "jss/api_object/purchasable"
585
+ require "jss/api_object/updatable"
586
+ require "jss/api_object/extendable"
587
+
588
+ ### Mix-in Sub Modules with Classes
589
+ require "jss/api_object/criteriable"
590
+ require "jss/api_object/scopable"
591
+
592
+ ### APIObject SubClasses with SubClasses
593
+ require "jss/api_object/advanced_search"
594
+ require "jss/api_object/extension_attribute"
595
+ require "jss/api_object/group"
596
+
597
+ ### APIObject SubClasses without SubClasses
598
+ require "jss/api_object/building"
599
+ require "jss/api_object/category"
600
+ require "jss/api_object/computer"
601
+ require "jss/api_object/department"
602
+ require "jss/api_object/distribution_point"
603
+ require "jss/api_object/ldap_server"
604
+ require "jss/api_object/mobile_device"
605
+ require "jss/api_object/netboot_server"
606
+ require "jss/api_object/network_segment"
607
+ require "jss/api_object/package"
608
+ require "jss/api_object/peripheral_type"
609
+ require "jss/api_object/peripheral"
610
+ require "jss/api_object/policy"
611
+ require "jss/api_object/removable_macaddr"
612
+ require "jss/api_object/script"
613
+ require "jss/api_object/site"
614
+ require "jss/api_object/software_update_server"
615
+ require "jss/api_object/user"
616
+