ruby-jss 0.6.3

Sign up to get free protection for your applications and to get access to all the features.

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
+