ruby-jss 0.14.0 → 1.0.0b2

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.

@@ -26,13 +26,30 @@
26
26
  ###
27
27
  module JSS
28
28
 
29
- # This is a stub - Patch data in the API is still borked.
30
- # Waiting for fixes from Jamf.
29
+ # An 'Internal' patch source. These sources are defined by
30
+ # Jamf themselves, as a part of the JSS, and cannot be created, modified
31
+ # or deleted.
31
32
  #
32
33
  # @see JSS::APIObject
33
34
  #
34
- class Patch < JSS::APIObject
35
+ class PatchInternalSource < JSS::PatchSource
35
36
 
36
- end # class Patch
37
+ # Constants
38
+ #####################################
39
+
40
+ ### The base for REST resources of this class
41
+ RSRC_BASE = 'patchinternalsources'.freeze
42
+
43
+ ### the hash key used for the JSON list output of all objects in the JSS
44
+ RSRC_LIST_KEY = :patch_internal_sources
45
+
46
+ # The hash key used for the JSON object output.
47
+ # It's also used in various error messages
48
+ RSRC_OBJECT_KEY = :patch_internal_source
49
+
50
+ ### these keys, as well as :id and :name, are present in valid API JSON data for this class
51
+ VALID_DATA_KEYS = %i[enabled endpoint].freeze
52
+
53
+ end # class PatchInternalSource
37
54
 
38
55
  end # module JSS
@@ -0,0 +1,480 @@
1
+ ### Copyright 2018 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ ###
27
+ module JSS
28
+
29
+ # An active Patch Software Title in the JSS.
30
+ #
31
+ # This class provides access to titles that have been added to Jamf Pro
32
+ # via a PatchInternalSource or a PatchExternalSource, and the versions
33
+ # contained therein.
34
+ #
35
+ # Patch versions for the title are available in the #versions read-only
36
+ # attribute, a Hash of versions keyed by the version string. The values are
37
+ # JSS::PatchTitle::Version objects.
38
+ #
39
+ # Use the patch_report method on the PatchTitle class, an instance of it, or
40
+ # a PatchTitle::Version, to retrieve a report of computers with a
41
+ # specific version of the title installed, or :all, :latest, or :unknown
42
+ # versions. Reports called on the class or an instance default to :all
43
+ # versions, and are slower to retrieve than a specific version,
44
+ #
45
+ # @see JSS::APIObject
46
+ #
47
+ class PatchTitle < JSS::APIObject
48
+
49
+ include JSS::Sitable
50
+ include JSS::Categorizable
51
+ include JSS::Creatable
52
+ include JSS::Updatable
53
+
54
+ # TODO: remove this and adjust parsing when jamf fixes the JSON
55
+ # Data map for PatchTitle XML data parsing cuz Borked JSON
56
+ # @see {JSS::XMLWorkaround} for details
57
+ USE_XML_WORKAROUND = {
58
+ patch_software_title: {
59
+ id: -1,
60
+ name: JSS::BLANK,
61
+ name_id: JSS::BLANK,
62
+ source_id: -1,
63
+ notifications: {
64
+ email_notification: nil,
65
+ web_notification: nil
66
+ },
67
+ category: {
68
+ id: -1,
69
+ name: JSS::BLANK
70
+ },
71
+ site: {
72
+ id: -1,
73
+ name: JSS::BLANK
74
+ },
75
+ versions: [
76
+ {
77
+ software_version: JSS::BLANK,
78
+ package: {
79
+ id: -1,
80
+ name: JSS::BLANK
81
+ }
82
+ }
83
+ ]
84
+ }
85
+ }.freeze
86
+
87
+ # TODO: remove this and adjust parsing when jamf fixes the JSON
88
+ # Data map for PatchReport XML data parsing cuz Borked JSON
89
+ # @see {JSS::XMLWorkaround} for details
90
+ PATCH_REPORT_DATA_MAP = {
91
+ patch_report: {
92
+ name: JSS::BLANK,
93
+ patch_software_title_id: -1,
94
+ total_computers: 0,
95
+ total_versions: 0,
96
+ versions: [
97
+ {
98
+ software_version: JSS::BLANK,
99
+ computers: [
100
+ {
101
+ id: -1,
102
+ name: JSS::BLANK,
103
+ mac_address: JSS::BLANK,
104
+ alt_mac_address: JSS::BLANK,
105
+ serial_number: JSS::BLANK
106
+ }
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ }.freeze
112
+
113
+ ### The base for REST resources of this class
114
+ RSRC_BASE = 'patchsoftwaretitles'.freeze
115
+
116
+ ### the hash key used for the JSON list output of all objects in the JSS
117
+ RSRC_LIST_KEY = :patch_software_titles
118
+
119
+ # The hash key used for the JSON object output.
120
+ # It's also used in various error messages
121
+ RSRC_OBJECT_KEY = :patch_software_title
122
+
123
+ ### these keys, as well as :id and :name, are present in valid API JSON data for this class
124
+ VALID_DATA_KEYS = %i[notifications name_id source_id].freeze
125
+
126
+ # the object type for this object in
127
+ # the object history table.
128
+ # See {APIObject#add_object_history_entry}
129
+ # TODO: comfirm this in 10.4
130
+ OBJECT_HISTORY_OBJECT_TYPE = 604
131
+
132
+ SITE_SUBSET = :top
133
+
134
+ # Where is the Category in the API JSON?
135
+ CATEGORY_SUBSET = :top
136
+
137
+ # How is the category stored in the API data?
138
+ CATEGORY_DATA_TYPE = Hash
139
+
140
+ # when fetching a specific version, this is a valid version
141
+ LATEST_VERSION_ID = 'Latest'.freeze
142
+
143
+ # when fetching a specific version, this is a valid version
144
+ UNKNOWN_VERSION_ID = 'Unknown'.freeze
145
+
146
+ REPORTS_RSRC_BASE = '/patchreports/patchsoftwaretitleid'.freeze
147
+
148
+ # Class Methods
149
+ #######################################
150
+
151
+ # The same as @see APIObject.all but also takes an optional
152
+ # source_id: parameter, which limites the results to
153
+ # patch titles with the specified source_id.
154
+ #
155
+ # ALSO, JAMF BUG: More broken json - the id is coming as a string.
156
+ # so here we turn it into an integer manually :-(
157
+ # Ditto for source_id
158
+ #
159
+ def self.all(refresh = false, source_id: nil, api: JSS.api)
160
+ data = super refresh, api: api
161
+ data.each do |info|
162
+ info[:id] = info[:id].to_i
163
+ info[:source_id] = info[:source_id].to_i
164
+ end
165
+ return data unless source_id
166
+ data.select { |p| p[:source_id] == source_id }
167
+ end
168
+
169
+ # The same as @see APIObject.all_names but also takes an optional
170
+ # source_id: parameter, which limites the results to
171
+ # patch titles with the specified source_id.
172
+ #
173
+ def self.all_names(refresh = false, source_id: nil, api: JSS.api)
174
+ all(refresh, source_id: source_id, api: api).map { |i| i[:name] }
175
+ end
176
+
177
+ # The same as @see APIObject.all_ids but also takes an optional
178
+ # source_id: parameter, which limites the results to
179
+ # patch titles with the specified source_id.
180
+ #
181
+ def self.all_ids(refresh = false, source_id: nil, api: JSS.api)
182
+ all(refresh, source_id: source_id, api: api).map { |i| i[:id] }
183
+ end
184
+
185
+ # @return [Array<String>] all 'name_id' values for active patches
186
+ #
187
+ def self.all_name_ids(refresh = false, source_id: nil, api: JSS.api)
188
+ all(refresh, source_id: source_id, api: api).map { |i| i[:name_id] }
189
+ end
190
+
191
+ # Returns an Array of unique source_ids used by active Patches
192
+ #
193
+ # e.g. if there are patches that come from one internal source
194
+ # and two external sources this might return [1,3,4].
195
+ #
196
+ # Regardless of how many patches come from each source, the
197
+ # source id appears only once in this array.
198
+ #
199
+ # @param refresh[Boolean] should the data be re-queried from the API?
200
+ #
201
+ # @param api[JSS::APIConnection] an API connection to use for the query.
202
+ # Defaults to the corrently active API. See {JSS::APIConnection}
203
+ #
204
+ # @return [Array<Integer>] the ids of the patch sources used in the JSS
205
+ #
206
+ def self.all_source_ids(refresh = false, api: JSS.api)
207
+ all(refresh, api: api).map { |i| i[:source_id] }.sort.uniq
208
+ end
209
+
210
+ # Get a patch report for a softwaretitle, withouth fetching an instance.
211
+ # Defaults to reporting all versions. Specifiying a version will be faster.
212
+ #
213
+ # The Hash returned has 3 keys:
214
+ # - :total_comptuters [Integer] total computers found for the requested version(s)
215
+ # - :total versions [Integer] How many versions does this title have?
216
+ # Always 1 if you report a specific version
217
+ # - :versions [Hash {String => Array<Hash>}] Keys are the version(s) requested
218
+ # values are Arrays of Hashes, one per computer with the keyed version
219
+ # installed. Computer Hashes have identifiers as keys.
220
+ #
221
+ # PatchTitle#patch_report calls this method, as does
222
+ # PatchTitle::Version.patch_report.
223
+ #
224
+ # @param title[Integer, String] The name or id of the software title to
225
+ # report.
226
+ #
227
+ # @param version[String,Symbol] Limit the report to this version.
228
+ # Can be a string version number like '8.13.2' or :latest, :unknown,
229
+ # or :all. Defaults to :all
230
+ #
231
+ # @param api[JSS::APIConnection] an API connection to use for the query.
232
+ # Defaults to the corrently active API. See {JSS::APIConnection}
233
+ #
234
+ # @return [Hash] the patch report for the version(s) specified.
235
+ #
236
+ def self.patch_report(title, version: :all, api: JSS.api)
237
+ title_id = valid_id title, api: api
238
+ raise JSS::NoSuchItemError, "No PatchTitle matches '#{title}'" unless title_id
239
+
240
+ rsrc = patch_report_rsrc title_id, version
241
+
242
+ # TODO: remove this and adjust parsing when jamf fixes the JSON
243
+ raw_report = XMLWorkaround.data_via_xml(rsrc, PATCH_REPORT_DATA_MAP, api)[:patch_report]
244
+ report = {}
245
+ report[:total_computers] = raw_report[:total_computers]
246
+ report[:total_versions] = raw_report[:total_versions]
247
+
248
+ if raw_report[:versions].is_a? Hash
249
+ vs = raw_report[:versions][:version][:software_version].to_s
250
+ comps = raw_report[:versions][:version][:computers]
251
+ comps = [] if comps.empty?
252
+ report[:versions] = { vs => comps }
253
+ return report
254
+ end
255
+
256
+ report[:versions] = {}
257
+ raw_report[:versions].each do |v|
258
+ report[:versions][v[:software_version].to_s] = v[:computers].empty? ? [] : v[:computers]
259
+ end
260
+ report
261
+ end
262
+
263
+ # aliases of patch_report
264
+ singleton_class.send(:alias_method, :version_report, :patch_report)
265
+ singleton_class.send(:alias_method, :report, :patch_report)
266
+
267
+ # given a requested version, return the rest rsrc for getting
268
+ # a patch report for it.
269
+ def self.patch_report_rsrc(id, vers)
270
+ case vers
271
+ when :all
272
+ "#{REPORTS_RSRC_BASE}/#{id}"
273
+ when :latest
274
+ "#{REPORTS_RSRC_BASE}/#{id}/version/#{LATEST_VERSION_ID}"
275
+ when :unknown
276
+ "#{REPORTS_RSRC_BASE}/#{id}/version/#{UNKNOWN_VERSION_ID}"
277
+ else
278
+ "#{REPORTS_RSRC_BASE}/#{id}/version/#{vers}"
279
+ end
280
+ end
281
+ private_class_method :patch_report_rsrc
282
+
283
+ # for some reason, patch titles can't be fetched by name.
284
+ # only by id. SO, look up the id if given a name.
285
+ #
286
+ def self.fetch(id: nil, name: nil, api: JSS.api)
287
+ unless id
288
+ id = JSS::PatchTitle.map_all_ids_to(:name).invert[name]
289
+ raise NoSuchItemError, "No matching #{self::RSRC_OBJECT_KEY} found" unless id
290
+ end
291
+
292
+ super id: id, api: api
293
+ end
294
+
295
+ # Attributes
296
+ #####################################
297
+
298
+ # @return [String] the 'name_id' for this patch title. name_id is a unique
299
+ # identfier provided by the patch source
300
+ attr_reader :name_id
301
+
302
+ # @return [Integer] the id of the patch source from which we get patches
303
+ # for this title
304
+ attr_reader :source_id
305
+
306
+ # @return [Boolean] Are new patches announced in the JSS web ui?
307
+ attr_reader :web_notification
308
+ alias web_notification? web_notification
309
+
310
+ # @return [Boolean] Are new patches announced by email?
311
+ attr_reader :email_notification
312
+ alias email_notification? email_notification
313
+
314
+ # @return [Hash{String => JSS::PatchTitle::Version}] The JSS::PatchVersions fetched for
315
+ # this title, keyed by version string
316
+ attr_reader :versions
317
+
318
+ # PatchTitles may be fetched by name: or id:
319
+ #
320
+ def initialize(**args)
321
+ super
322
+
323
+ @name_id = @init_data[:name_id]
324
+ @source_id = @init_data[:source_id]
325
+
326
+ @init_data[:notifications] ||= {}
327
+ notifs = @init_data[:notifications]
328
+ @web_notification = notifs[:web_notification].nil? ? false : notifs[:web_notification]
329
+ @email_notification = notifs[:email_notification].nil? ? false : notifs[:email_notification]
330
+
331
+ @versions = {}
332
+ @init_data[:versions] ||= []
333
+ @init_data[:versions].each do |vers|
334
+ @versions[vers[:software_version]] = JSS::PatchTitle::Version.new(self, vers)
335
+ end # each do vers
336
+
337
+ @changed_pkgs = []
338
+ end
339
+
340
+ # @return [Hash] Subset of @versions, containing those which have packages
341
+ # assigned
342
+ #
343
+ def versions_with_packages
344
+ versions.select { |_ver_string, vers| vers.package_assigned? }
345
+ end
346
+
347
+ # Set email notifications on or off
348
+ #
349
+ # @param new_setting[Boolean] Should email notifications be on or off?
350
+ #
351
+ # @return [void]
352
+ #
353
+ def email_notification=(new_setting)
354
+ return if email_notification == new_setting
355
+ raise JSS::InvalidDataError, 'New Setting must be boolean true or false' unless JSS::TRUE_FALSE.include? @email_notification = new_setting
356
+ @need_to_update = true
357
+ end
358
+
359
+ # Set web notifications on or off
360
+ #
361
+ # @param new_setting[Boolean] Should email notifications be on or off?
362
+ #
363
+ # @return [void]
364
+ #
365
+ def web_notification=(new_setting)
366
+ return if web_notification == new_setting
367
+ raise JSS::InvalidDataError, 'New Setting must be boolean true or false' unless JSS::TRUE_FALSE.include? @web_notification = new_setting
368
+ @need_to_update = true
369
+ end
370
+
371
+ # this is called by JSS::PatchTitle::Version#package= to update @changed_pkgs which
372
+ # is used by #rest_xml to change the package assigned to a patch version
373
+ # in this title.
374
+ def changed_pkg_for_version(version)
375
+ @changed_pkgs << version
376
+ @need_to_update = true
377
+ end
378
+
379
+ def source_id=(new_id)
380
+ sid = JSS::PatchSource.valid_patch_source_id new_id
381
+ raise JSS::NoSuchItemError, "No active Patch Sources matche '#{new_id}'" unless sid
382
+ return if sid == source_id
383
+ @source_id = sid
384
+ @need_to_update = true
385
+ end
386
+
387
+ def name_id=(new_id)
388
+ return if new_id == name_id
389
+ raise JSS::MissingDataError, 'source_id must be set before setting name_id' if source_id.to_s.empty?
390
+ raise JSS::NoSuchItemError, "source_id #{source_id} doesn't offer name_id '#{new_id}'" unless JSS::PatchSource.available_name_ids(source_id).include? new_id
391
+ @name_id = new_id
392
+ @need_to_update = true
393
+ end
394
+
395
+ # wrapper to fetch versions after creating
396
+ def create
397
+ validate_for_saving
398
+ response = super
399
+ @versions = self.class.fetch(id: id).versions
400
+ response
401
+ end
402
+
403
+ # wrapper to clear @changed_pkgs after updating
404
+ def update
405
+ validate_for_saving
406
+ response = super
407
+ @changed_pkgs.clear
408
+ response
409
+ end
410
+
411
+ # Get a patch report for this title.
412
+ #
413
+ # See the class method JSS::PatchTitle.patch_report
414
+ #
415
+ def patch_report(vers = :all)
416
+ JSS::PatchTitle.patch_report id, version: vers, api: @api
417
+ end
418
+ alias version_report patch_report
419
+ alias report patch_report
420
+
421
+ # Remove the various cached data
422
+ # from the instance_variables used to create
423
+ # pretty-print (pp) output.
424
+ #
425
+ # @return [Array] the desired instance_variables
426
+ #
427
+ def pretty_print_instance_variables
428
+ vars = super
429
+ vars.delete :@versions
430
+ vars
431
+ end
432
+
433
+ #################################
434
+ private
435
+
436
+ def validate_for_saving
437
+ raise JSS::MissingDataError, 'PatchTitles must have valid source_id and name_id' if source_id.to_s.empty? || name_id.to_s.empty?
438
+ end
439
+
440
+ # Return the REST XML for this title, with the current values,
441
+ # for saving or updating.
442
+ #
443
+ def rest_xml
444
+ doc = REXML::Document.new # JSS::APIConnection::XML_HEADER
445
+ obj = doc.add_element RSRC_OBJECT_KEY.to_s
446
+
447
+ obj.add_element('name').text = name
448
+ obj.add_element('name_id').text = name_id
449
+ obj.add_element('source_id').text = source_id
450
+
451
+ notifs = obj.add_element 'notifications'
452
+ notifs.add_element('web_notification').text = web_notification?.to_s
453
+ notifs.add_element('email_notification').text = email_notification?.to_s
454
+
455
+ add_changed_pkg_xml obj unless @changed_pkgs.empty?
456
+
457
+ add_category_to_xml doc
458
+ add_site_to_xml doc
459
+
460
+ doc.to_s
461
+ end # rest_xml
462
+
463
+ # add xml for any package changes to patch versions
464
+ def add_changed_pkg_xml(obj)
465
+ versions_elem = obj.add_element 'versions'
466
+ @changed_pkgs.each do |vers|
467
+ velem = versions_elem.add_element 'version'
468
+ velem.add_element('software_version').text = vers.to_s
469
+ pkg = velem.add_element 'package'
470
+ # leave am empty package element to remove the pkg assignement
471
+ next if versions[vers].package_id == :none
472
+ pkg.add_element('id').text = versions[vers].package_id.to_s
473
+ end # do vers
474
+ end
475
+
476
+ end # class Patch
477
+
478
+ end # module JSS
479
+
480
+ require 'jss/api_object/patch_title/version'