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.
- checksums.yaml +4 -4
- data/CHANGES.md +31 -0
- data/lib/jss.rb +5 -4
- data/lib/jss/api_connection.rb +67 -33
- data/lib/jss/api_object.rb +86 -34
- data/lib/jss/api_object/categorizable.rb +53 -28
- data/lib/jss/api_object/configuration_profile.rb +7 -0
- data/lib/jss/api_object/creatable.rb +6 -11
- data/lib/jss/api_object/criteriable/criterion.rb +9 -7
- data/lib/jss/api_object/distribution_point.rb +1 -0
- data/lib/jss/api_object/group.rb +177 -233
- data/lib/jss/api_object/mobile_device_application.rb +6 -0
- data/lib/jss/api_object/package.rb +8 -1
- data/lib/jss/api_object/patch_policy.rb +588 -2
- data/lib/jss/api_object/patch_source.rb +357 -0
- data/lib/jss/api_object/patch_source/patch_external_source.rb +156 -0
- data/lib/jss/api_object/{patch.rb → patch_source/patch_internal_source.rb} +21 -4
- data/lib/jss/api_object/patch_title.rb +480 -0
- data/lib/jss/api_object/patch_title/version.rb +171 -0
- data/lib/jss/api_object/policy.rb +780 -679
- data/lib/jss/api_object/scopable/scope.rb +18 -13
- data/lib/jss/api_object/script.rb +7 -0
- data/lib/jss/api_object/self_servable.rb +417 -70
- data/lib/jss/api_object/self_servable/icon.rb +12 -0
- data/lib/jss/api_object/sitable.rb +4 -4
- data/lib/jss/api_object/updatable.rb +2 -2
- data/lib/jss/exceptions.rb +22 -19
- data/lib/jss/ruby_extensions/hash.rb +18 -13
- data/lib/jss/utility.rb +7 -71
- data/lib/jss/validate.rb +68 -3
- data/lib/jss/version.rb +1 -1
- data/lib/jss/xml_workaround.rb +208 -0
- metadata +10 -5
@@ -179,6 +179,18 @@ module JSS
|
|
179
179
|
path.jss_save @data
|
180
180
|
end
|
181
181
|
|
182
|
+
# Remove the data object from
|
183
|
+
# the instance_variables used to create
|
184
|
+
# pretty-print (pp) output.
|
185
|
+
#
|
186
|
+
# @return [Array] the desired instance_variables
|
187
|
+
#
|
188
|
+
def pretty_print_instance_variables
|
189
|
+
vars = instance_variables.sort
|
190
|
+
vars.delete :@data
|
191
|
+
vars
|
192
|
+
end
|
193
|
+
|
182
194
|
end # class icon
|
183
195
|
|
184
196
|
end # module
|
@@ -80,7 +80,7 @@ module JSS
|
|
80
80
|
# @return [String] The name of the site for this object.
|
81
81
|
#
|
82
82
|
def site_name
|
83
|
-
@site_name
|
83
|
+
@site_name || NO_SITE_NAME
|
84
84
|
end # cat name
|
85
85
|
alias site site_name
|
86
86
|
|
@@ -89,7 +89,7 @@ module JSS
|
|
89
89
|
# @return [Integer] The id of the site for this object.
|
90
90
|
#
|
91
91
|
def site_id
|
92
|
-
@site_id
|
92
|
+
@site_id || NO_SITE_ID
|
93
93
|
end # cat id
|
94
94
|
|
95
95
|
# The JSS::Site instance for this object's site
|
@@ -165,7 +165,7 @@ module JSS
|
|
165
165
|
elsif @init_data[self.class::SITE_SUBSET]
|
166
166
|
@init_data[self.class::SITE_SUBSET][:site]
|
167
167
|
end
|
168
|
-
site_data ||= {}
|
168
|
+
site_data ||= { name: NO_SITE_NAME, id: NO_SITE_ID }
|
169
169
|
|
170
170
|
@site_name = site_data[:name]
|
171
171
|
@site_id = site_data[:id]
|
@@ -187,7 +187,7 @@ module JSS
|
|
187
187
|
parent_elem ||= root.add_element(self.class::SITE_SUBSET.to_s)
|
188
188
|
parent_elem.add_element 'site'
|
189
189
|
end
|
190
|
-
site_elem.add_element('name').text =
|
190
|
+
site_elem.add_element('name').text = site_name.to_s
|
191
191
|
end # add_site_to_xml
|
192
192
|
|
193
193
|
end # module categorizable
|
@@ -76,7 +76,7 @@ module JSS
|
|
76
76
|
#
|
77
77
|
def name=(newname)
|
78
78
|
return nil if @name == newname
|
79
|
-
raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless
|
79
|
+
raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless updatable?
|
80
80
|
raise JSS::InvalidDataError, "Names can't be empty!" if newname.to_s.empty?
|
81
81
|
raise JSS::AlreadyExistsError, "A #{self.class::RSRC_OBJECT_KEY} named '#{newname}' already exsists in the JSS" \
|
82
82
|
if self.class.all_names(:refresh, api: @api).include? newname
|
@@ -91,7 +91,7 @@ module JSS
|
|
91
91
|
#
|
92
92
|
def update
|
93
93
|
return nil unless @need_to_update
|
94
|
-
raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless
|
94
|
+
raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless updatable?
|
95
95
|
raise JSS::NoSuchItemError, "Not In JSS! Use #create to create this #{self.class::RSRC_OBJECT_KEY} in the JSS before updating it." unless @in_jss
|
96
96
|
@api.put_rsrc @rest_rsrc, rest_xml
|
97
97
|
@need_to_update = false
|
data/lib/jss/exceptions.rb
CHANGED
@@ -26,79 +26,82 @@
|
|
26
26
|
###
|
27
27
|
module JSS
|
28
28
|
|
29
|
-
#####################################
|
30
29
|
### Exceptions
|
31
30
|
#####################################
|
32
31
|
|
33
|
-
###
|
34
32
|
### MissingDataError - raise this error when we
|
35
33
|
### are missing args, or other simliar stuff.
|
36
34
|
###
|
37
35
|
class MissingDataError < RuntimeError; end
|
38
36
|
|
39
|
-
###
|
40
37
|
### InvalidDataError - raise this error when
|
41
38
|
### a data item isn't what we expected.
|
42
39
|
###
|
43
40
|
class InvalidDataError < RuntimeError; end
|
44
41
|
|
45
|
-
###
|
46
42
|
### InvalidConnectionError - raise this error when we
|
47
43
|
### don't have a usable connection to a network service, or
|
48
44
|
### don't have proper authentication/authorization.
|
49
45
|
###
|
50
46
|
class InvalidConnectionError < RuntimeError; end
|
51
47
|
|
52
|
-
###
|
53
48
|
### NoSuchItemError - raise this error when
|
54
49
|
### a desired item doesn't exist.
|
55
50
|
###
|
56
51
|
class NoSuchItemError < RuntimeError; end
|
57
52
|
|
58
|
-
###
|
59
53
|
### AlreadyExistsError - raise this error when
|
60
54
|
### trying to create something that already exists.
|
61
55
|
###
|
62
56
|
class AlreadyExistsError < RuntimeError; end
|
63
57
|
|
64
|
-
###
|
65
58
|
### FileServiceError - raise this error when
|
66
59
|
### there's a problem accessing file service on a
|
67
60
|
### distribution point.
|
68
61
|
###
|
69
62
|
class FileServiceError < RuntimeError; end
|
70
63
|
|
71
|
-
###
|
72
64
|
### UnmanagedError - raise this when we
|
73
65
|
### try to do something managerial to
|
74
66
|
### an unmanaged object
|
75
67
|
###
|
76
|
-
class UnmanagedError <
|
68
|
+
class UnmanagedError < RuntimeError; end
|
77
69
|
|
78
|
-
###
|
79
70
|
### UnsupportedError - raise this when we
|
80
71
|
### try to do something not yet supported
|
81
72
|
###
|
82
|
-
class UnsupportedError <
|
73
|
+
class UnsupportedError < RuntimeError; end
|
83
74
|
|
84
|
-
###
|
85
75
|
### TimeoutError - raise this when we
|
86
76
|
### try to do and it times out
|
87
77
|
###
|
88
|
-
class TimeoutError <
|
78
|
+
class TimeoutError < RuntimeError; end
|
89
79
|
|
90
|
-
###
|
91
80
|
### AuthenticationError - raise this when
|
92
81
|
### a name/pw are wrong
|
93
82
|
###
|
94
|
-
class AuthenticationError <
|
83
|
+
class AuthenticationError < RuntimeError; end
|
95
84
|
|
96
|
-
###
|
97
85
|
### ConflictError - raise this when
|
98
86
|
### attempts to PUT or PUSH to the API
|
99
87
|
### result in a 409 Conflict http error.
|
100
|
-
### See JSS::
|
88
|
+
### See {JSS::APIConnection#raise_conflict_error}
|
89
|
+
###
|
90
|
+
class ConflictError < RuntimeError; end
|
91
|
+
|
92
|
+
### BadRequestError - raise this when
|
93
|
+
### attempts to PUT or PUSH or DELETE to the API
|
94
|
+
### result in a 400 Bad Request http error.
|
95
|
+
### See {JSS::APIConnection.raise_bad_request_error}
|
96
|
+
###
|
97
|
+
class BadRequestError < RuntimeError; end
|
98
|
+
|
99
|
+
### APIRequestError - raise this when
|
100
|
+
### attempts API actions generate an error not dealt with
|
101
|
+
### by ConflictError or BadRequestError
|
102
|
+
### result in a 400 Bad Request http error.
|
103
|
+
### See {JSS::APIConnection.raise_api_error}
|
101
104
|
###
|
102
|
-
class
|
105
|
+
class APIRequestError < RuntimeError; end
|
103
106
|
|
104
|
-
end
|
107
|
+
end # module JSS
|
@@ -78,7 +78,7 @@ class Hash
|
|
78
78
|
|
79
79
|
# Since a lot of JSON data from the API comes as deeply-nested structures
|
80
80
|
# of Hashes and Arrays, it can be a pain to reference some of the deeper
|
81
|
-
# data inside, and it isn't worth coding them out into
|
81
|
+
# data inside, and it isn't worth coding them out into instance attributes.
|
82
82
|
#
|
83
83
|
# For example see the 'hardware' subset of a JSS::Computer's API data,
|
84
84
|
# which is stored as a Hash in the {JSS::Computer.hardware} attribute.
|
@@ -94,7 +94,7 @@ class Hash
|
|
94
94
|
# But, there are two problems with just storing #hardware as an OpenStruct:
|
95
95
|
# 1) we'd lose some important Hash methods, like #keys and #values, breaking
|
96
96
|
# backward compatibility. 2) OpenStructs only work on the Hash itself, not
|
97
|
-
#
|
97
|
+
# its contents.
|
98
98
|
#
|
99
99
|
# So to get the best of both worlds, we use the RecursiveOpenStruct gem
|
100
100
|
#
|
@@ -112,20 +112,25 @@ class Hash
|
|
112
112
|
# CAVEAT: Treat these as read-only.
|
113
113
|
#
|
114
114
|
# While the Hashes themselves may be mutable, their use in ruby-jss Classes
|
115
|
-
# should
|
116
|
-
# object created by this method should
|
117
|
-
# or the RecursiveOpenStruct are NOT synced between them
|
115
|
+
# should usually be considered read-only - neither the Hash, nor the
|
116
|
+
# RecursiveOpenStruct object created by this method should be changed.
|
117
|
+
# Changes to the Hash or the RecursiveOpenStruct are NOT synced between them,
|
118
|
+
# and ruby-jss won't know to send such changes back to the API when #update
|
119
|
+
# is called.
|
118
120
|
#
|
119
121
|
# This should be fine for the intended uses. Data like Computer#hardware
|
120
122
|
# isn't sent back to the JSS via Computer#update, since it must come
|
121
|
-
# from a 'recon' anyway.
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
123
|
+
# from a 'recon' anyway.
|
124
|
+
#
|
125
|
+
# Data that is sent back to the JSS will have setter methods defined in the
|
126
|
+
# class or a mixin module (e.g. the Locatable module).
|
127
|
+
#
|
128
|
+
# Since the data is functionally read-only, why not use the ImmutableStruct
|
129
|
+
# gem, used elsewhere in ruby-jss?
|
130
|
+
#
|
131
|
+
# Because ImmutableStruct is really for creating fully-fleshed-out read-only
|
132
|
+
# classes, with a known set of attributes rather than just giving us a nicer
|
133
|
+
# way to access Hash data with arbitrary keys.
|
129
134
|
#
|
130
135
|
def jss_recursive_ostruct
|
131
136
|
@jss_ros ||= RecursiveOpenStruct.new(self, recurse_over_arrays: true)
|
data/lib/jss/utility.rb
CHANGED
@@ -415,35 +415,17 @@ module JSS
|
|
415
415
|
|
416
416
|
# Parse a JSS Version number into something comparable.
|
417
417
|
#
|
418
|
-
# With Jamf Pro 9.99, "Semantic Versioning" is used, see http://semver.org/
|
419
|
-
#
|
420
|
-
# For versions less than 9.99 parsing is like this:
|
421
|
-
# - Digits before the first dot are the Major Version
|
422
|
-
# - The first digit after the first dot is the Minor Version
|
423
|
-
# - Any other digits after the first dot but before a non-digit
|
424
|
-
# are the Revision
|
425
|
-
# - Anything after a second dot is the build identifier
|
426
|
-
# - Any non-digit anywhere means that it and everything after it
|
427
|
-
# are the build identifier
|
428
|
-
#
|
429
|
-
# So 9.32 becomes major-9, minor-3, rev-2, build-''
|
430
|
-
# and 9.32.3764 becomes major-9, minor-3, rev-2, build-3764
|
431
|
-
# and 9.32a3764 becomes major-9, minor-3, rev-2, build-a3764
|
432
|
-
# and 9.32a1234.t234 becomes major-9, minor-3, rev-2, build-a1234.t234
|
433
|
-
#
|
434
|
-
# This old style method of parsing will break if digits between the first
|
435
|
-
# dot and the second (or the end) ever gets above 99, since '100' will
|
436
|
-
# become minor-1, rev-0
|
437
|
-
#
|
438
418
|
# This method returns a Hash with these keys:
|
439
419
|
# * :major => the major version, Integer
|
440
420
|
# * :minor => the minor version, Integor
|
441
|
-
# * :maint => the revision, Integer
|
442
|
-
# (this is also available with the keys :patch and :revision)
|
421
|
+
# * :maint => the revision, Integer (also available as :patch and :revision)
|
443
422
|
# * :build => the revision, String
|
444
423
|
# * :version => a Gem::Version object built from :major, :minor, :revision
|
445
424
|
# which can be easily compared with other Gem::Version objects.
|
446
425
|
#
|
426
|
+
# NOTE: the :version value ignores build numbers, so comparisons
|
427
|
+
# only compare major.minor.maint
|
428
|
+
#
|
447
429
|
# @param version[String] a JSS version number from the API
|
448
430
|
#
|
449
431
|
# @return [Hash{Symbol => String, Gem::Version}] the parsed version data.
|
@@ -452,17 +434,8 @@ module JSS
|
|
452
434
|
major, second_part, *_rest = version.split('.')
|
453
435
|
raise JSS::InvalidDataError, 'JSS Versions must start with "x.x" where x is one or more digits' unless major =~ /\d$/ && second_part =~ /^\d/
|
454
436
|
|
455
|
-
|
456
|
-
if major == '9' && (second_part.to_i < 99)
|
457
|
-
parse_jss_version_oldstyle version
|
458
|
-
else
|
459
|
-
parse_jss_version_newstyle version
|
460
|
-
end
|
461
|
-
end
|
437
|
+
release, build = version.split(/-/)
|
462
438
|
|
463
|
-
# (see parse_jss_version)
|
464
|
-
def self.parse_jss_version_newstyle(version)
|
465
|
-
release, build = version.split '-'
|
466
439
|
major, minor, revision = release.split '.'
|
467
440
|
minor ||= 0
|
468
441
|
revision ||= 0
|
@@ -474,46 +447,9 @@ module JSS
|
|
474
447
|
maint: revision.to_i,
|
475
448
|
patch: revision.to_i,
|
476
449
|
build: build,
|
477
|
-
|
478
|
-
version: Gem::Version.new("#{major}.#{minor}.#{revision}#{build}")
|
479
|
-
}
|
480
|
-
end # parse_jss_version_oldstyle
|
481
|
-
|
482
|
-
# (see parse_jss_version)
|
483
|
-
def self.parse_jss_version_oldstyle(version)
|
484
|
-
version =~ /^(\d+?)\.(.*)$/
|
485
|
-
major = Regexp.last_match[1]
|
486
|
-
second_part = Regexp.last_match[2].to_s
|
487
|
-
|
488
|
-
minor = second_part[0]
|
489
|
-
revision = second_part[1..-1]
|
490
|
-
|
491
|
-
# if there's a non-digit anywhere in any part, it and everything after
|
492
|
-
# is the build.
|
493
|
-
if revision.to_s =~ /^(\d*)(\D.*)$/
|
494
|
-
revision = Regexp.last_match[1]
|
495
|
-
build = Regexp.last_match[2]
|
496
|
-
# but remove a leading dot
|
497
|
-
build = build[1..-1] if build.start_with? '.'
|
498
|
-
end
|
499
|
-
minor ||= ''
|
500
|
-
revision ||= ''
|
501
|
-
|
502
|
-
version_string = major.to_s
|
503
|
-
unless minor.empty?
|
504
|
-
version_string << ".#{minor}"
|
505
|
-
version_string << ".#{revision}" unless revision.empty?
|
506
|
-
end
|
507
|
-
{
|
508
|
-
major: major.to_i,
|
509
|
-
minor: minor.to_i,
|
510
|
-
revision: revision.to_i,
|
511
|
-
maint: revision.to_i,
|
512
|
-
patch: revision.to_i,
|
513
|
-
build: build.to_s,
|
514
|
-
version: Gem::Version.new(version_string)
|
450
|
+
version: Gem::Version.new("#{major}.#{minor}.#{revision}")
|
515
451
|
}
|
516
|
-
end
|
452
|
+
end
|
517
453
|
|
518
454
|
# @return [Boolean] is this code running as root?
|
519
455
|
#
|
data/lib/jss/validate.rb
CHANGED
@@ -22,7 +22,6 @@
|
|
22
22
|
#
|
23
23
|
#
|
24
24
|
|
25
|
-
#
|
26
25
|
module JSS
|
27
26
|
|
28
27
|
# A collection of methods for validating values. Mostly for
|
@@ -60,13 +59,18 @@ module JSS
|
|
60
59
|
ok = true
|
61
60
|
parts = val.strip.split '.'
|
62
61
|
ok = false unless parts.size == 4
|
63
|
-
parts.each { |p| ok = false unless p.jss_integer? && p.to_i < 256 }
|
62
|
+
parts.each { |p| ok = false unless p.jss_integer? && p.to_i < 256 && p.to_i >= 0 }
|
64
63
|
raise JSS::InvalidDataError, "Not a valid IPv4 address: '#{val}'" unless ok
|
65
64
|
val
|
66
65
|
end
|
67
66
|
|
68
67
|
# Validate that a value doesn't already exist for a given identifier of a given class
|
69
68
|
#
|
69
|
+
# e.g. when klass = JSS::Computer, identifier = :name, and val = 'foo'
|
70
|
+
# will raise an error when a computer named 'foo' exists
|
71
|
+
#
|
72
|
+
# Otherwise returns val.
|
73
|
+
#
|
70
74
|
# @param klass[JSS::APIObject] A subclass of JSS::APIObject, e.g. JSS::Computer
|
71
75
|
#
|
72
76
|
# @param identifier[Symbol] One of the keys of an Item of the class's #all Array
|
@@ -76,10 +80,71 @@ module JSS
|
|
76
80
|
# @return [Object] the validated unique value
|
77
81
|
#
|
78
82
|
def self.unique_identifier(klass, identifier, val, api: JSS.api)
|
79
|
-
|
83
|
+
return val unless klass.all(:refresh, api: api).map { |i| i[identifier] }.include? val
|
84
|
+
raise JSS::AlreadyExistsError, "A #{klass} already exists with #{identifier} '#{val}'"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Confirm that the given value is a boolean value, accepting
|
88
|
+
# strings and symbols and returning real booleans as needed
|
89
|
+
# Accepts: true, false, 'true', 'false', :true, :false, 'yes', 'no', :yes,
|
90
|
+
# or :no (all Strings and Symbols are case insensitive)
|
91
|
+
#
|
92
|
+
# TODO: use this throughout ruby-jss
|
93
|
+
#
|
94
|
+
# @param bool [Boolean,String,Symbol] The value to validate
|
95
|
+
#
|
96
|
+
# @return [Boolean] the valid boolean
|
97
|
+
#
|
98
|
+
def self.boolean(bool)
|
99
|
+
return bool if JSS::TRUE_FALSE.include? bool
|
100
|
+
return true if bool.to_s =~ /^(true|yes)$/i
|
101
|
+
return false if bool.to_s =~ /^(false|no)$/i
|
102
|
+
raise JSS::InvalidDataError, 'Value must be boolean true or false'
|
103
|
+
end
|
104
|
+
|
105
|
+
# Confirm that a value is an integer or a string representation of an
|
106
|
+
# integer. Return the integer, or raise an error
|
107
|
+
#
|
108
|
+
# TODO: use this throughout ruby-jss
|
109
|
+
#
|
110
|
+
# @param val[Object] the value to validate
|
111
|
+
#
|
112
|
+
# @return [void]
|
113
|
+
#
|
114
|
+
def self.integer(val)
|
115
|
+
val = val.to_i if val.is_a?(String) && val.jss_integer?
|
116
|
+
raise JSS::InvalidDataError, 'Value must be an integer' unless val.is_a? Integer
|
117
|
+
val
|
118
|
+
end
|
119
|
+
|
120
|
+
# validate that the given value is a non-empty string
|
121
|
+
#
|
122
|
+
# @param val [Object] the thing to validate
|
123
|
+
#
|
124
|
+
# @return [String] the valid non-empty string
|
125
|
+
#
|
126
|
+
def self.non_empty_string(val)
|
127
|
+
raise JSS::InvalidDataError, 'value must be a non-empty String' unless val.is_a?(String) && !val.empty?
|
80
128
|
val
|
81
129
|
end
|
82
130
|
|
131
|
+
# Confirm that the given value is a boolean value, accepting
|
132
|
+
# strings and symbols and returning real booleans as needed
|
133
|
+
# Accepts: true, false, 'true', 'false', :true, :false, 'yes', 'no', :yes,
|
134
|
+
# or :no (all Strings and Symbols are case insensitive)
|
135
|
+
#
|
136
|
+
#
|
137
|
+
# @param bool [Boolean,String,Symbol] The value to validate
|
138
|
+
#
|
139
|
+
# @return [Boolean] the valid boolean
|
140
|
+
#
|
141
|
+
def self.boolean(bool)
|
142
|
+
return bool if JSS::TRUE_FALSE.include? bool
|
143
|
+
return true if bool.to_s =~ /^(true|yes)$/i
|
144
|
+
return false if bool.to_s =~ /^(false|no)$/i
|
145
|
+
raise JSS::InvalidDataError, 'Value must be boolean true or false'
|
146
|
+
end
|
147
|
+
|
83
148
|
end # module validate
|
84
149
|
|
85
150
|
end # module JSS
|
data/lib/jss/version.rb
CHANGED
@@ -0,0 +1,208 @@
|
|
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
|
+
# Since a non-trivial amounts of the JSON data from the API are borked, the
|
30
|
+
# methods here can be used to parse the XML data into usable JSON, which we
|
31
|
+
# can then treat normally.
|
32
|
+
#
|
33
|
+
# For classes with borked JSON, set the constant USE_XML_WORKAROUND to a Hash
|
34
|
+
# with a single key that maps the structure of the XML and resultant Ruby data.
|
35
|
+
#
|
36
|
+
# As an example, here's the data map from JSS::PatchTitle
|
37
|
+
#
|
38
|
+
# USE_XML_WORKAROUND = {
|
39
|
+
# patch_software_title: {
|
40
|
+
# id: -1,
|
41
|
+
# name: JSS::BLANK,
|
42
|
+
# name_id: JSS::BLANK,
|
43
|
+
# source_id: -1,
|
44
|
+
# notifications: {
|
45
|
+
# email_notification: nil,
|
46
|
+
# web_notification: nil
|
47
|
+
# },
|
48
|
+
# category: {
|
49
|
+
# id: -1,
|
50
|
+
# name: JSS::BLANK
|
51
|
+
# },
|
52
|
+
# versions: [
|
53
|
+
# {
|
54
|
+
# software_version: JSS::BLANK,
|
55
|
+
# package: -1,
|
56
|
+
# name: JSS::BLANK
|
57
|
+
# }
|
58
|
+
# }
|
59
|
+
# ]
|
60
|
+
# }
|
61
|
+
# }.freeze
|
62
|
+
#
|
63
|
+
# The constant must always be a hash that represents the data structure
|
64
|
+
# of the object. The keys match the names of the XML elements, and the
|
65
|
+
# values indicate how to handle the element values.
|
66
|
+
#
|
67
|
+
# Single-value attributes will be converted based on the provided map example
|
68
|
+
# The class of the map example is the class of the desired data, and the value
|
69
|
+
# of the map example is the value to use when the XML data is nil or empty.
|
70
|
+
#
|
71
|
+
# So a map example of '' (an empty string, a.k.a. JSS::BLANK) indicates
|
72
|
+
# that the value should be a String and if the XML element is nil or empty,
|
73
|
+
# use '' in the Ruby data. If its -1, that means the value should be an
|
74
|
+
# Integer, and if its empty or nil, use -1 in Ruby.
|
75
|
+
#
|
76
|
+
# Booleans are special: the map example must be nil, and nil is used when the
|
77
|
+
# xml is empty, since you want to be able to know that the XML value was
|
78
|
+
# neither true nor false.
|
79
|
+
#
|
80
|
+
# Allowed single value classes and common default examples are:
|
81
|
+
# String, common default: '' or JSS::BLANK
|
82
|
+
# Integer, common default: -1
|
83
|
+
# Float, common default: -1.0
|
84
|
+
# Boolean, required default: nil
|
85
|
+
#
|
86
|
+
# Arrays and Hashes will be recognized as such, and their contents will be
|
87
|
+
# converted recursively using the same process.
|
88
|
+
#
|
89
|
+
# For Arrays, provide one example in the map of an Array
|
90
|
+
# item, and all sub elements will be processd like the example. See
|
91
|
+
# the ':versions' array defiend in the example above
|
92
|
+
#
|
93
|
+
# For sub-hashes, use the same technique as for the main hash.
|
94
|
+
# see the :category value above.
|
95
|
+
#
|
96
|
+
# IMPORTANT NOTE: Lots of Arrays in the XML have a matching 'size' element
|
97
|
+
# containing an integer indicating how many items are in the array. Unfortunately
|
98
|
+
# there is zero consistency about their existence or location. If they exist at
|
99
|
+
# all, sometimes the are adjacent to the Array element, sometimes within it.
|
100
|
+
#
|
101
|
+
# Fortunately in Ruby, all container/enumerable classes have a 'size' or 'count'
|
102
|
+
# method to easily get that number.
|
103
|
+
# As such, when parsing XML elements, any 'size' element that exists with no
|
104
|
+
# other 'size' elements, and contains only an integer value and no sub-
|
105
|
+
# elements, are ignored. I haven't yet found any cases of a 'size' element
|
106
|
+
# that is used for anything else.
|
107
|
+
#
|
108
|
+
module XMLWorkaround
|
109
|
+
|
110
|
+
BOOLEAN_STRINGS = %w[true false].freeze
|
111
|
+
TRUE_STRING = BOOLEAN_STRINGS.first
|
112
|
+
SIZE_ELEM_NAME = 'size'.freeze
|
113
|
+
|
114
|
+
# When APIObject classes are fetched, API JSON data is retrieved by the
|
115
|
+
# APIObject#lookup_object_data method, which parses the JSON into Ruby data.
|
116
|
+
#
|
117
|
+
# If the APIObject class has the constant USE_XML_WORKAROUND defined, that
|
118
|
+
# means the JSON data from the API is invalid, incorrect, or otherwise
|
119
|
+
# borked. So instead, the XML is retrieved from the API here.
|
120
|
+
#
|
121
|
+
# It is then parsed by using the methods in this module and returned
|
122
|
+
# to the APIObject#lookup_object_data method, which then
|
123
|
+
# treats it normally.
|
124
|
+
#
|
125
|
+
def self.data_via_xml(rsrc, map, api)
|
126
|
+
raw_xml = api.get_rsrc(rsrc, :xml)
|
127
|
+
xmlroot = REXML::Document.new(raw_xml).root
|
128
|
+
hash_from_xml = {}
|
129
|
+
map.each do |key, model|
|
130
|
+
hash_from_xml[key] = process_map_item model, xmlroot
|
131
|
+
end
|
132
|
+
hash_from_xml
|
133
|
+
end
|
134
|
+
|
135
|
+
# given a REXML element, return its ruby value
|
136
|
+
#
|
137
|
+
# This method is then called recursively as needed when traversing XML
|
138
|
+
# elements that contain sub-elements.
|
139
|
+
#
|
140
|
+
# XML Elements that do not contain other elements are converted to a
|
141
|
+
# single ruby value.
|
142
|
+
#
|
143
|
+
def self.process_map_item(model, element)
|
144
|
+
case model
|
145
|
+
when String
|
146
|
+
element ? element.text : model
|
147
|
+
when Integer
|
148
|
+
element ? element.text.to_i : model
|
149
|
+
when Float
|
150
|
+
element ? element.text.to_f : model
|
151
|
+
when nil
|
152
|
+
return nil unless element
|
153
|
+
element.text.downcase == TRUE_STRING ? true : false
|
154
|
+
when Array
|
155
|
+
element ? elem_as_array(model.first, element) : []
|
156
|
+
when Hash
|
157
|
+
element ? elem_as_hash(model, element) : {}
|
158
|
+
end # case type
|
159
|
+
end
|
160
|
+
|
161
|
+
# remove the 'size' sub element from a given element as long as:
|
162
|
+
# - a sub element named 'size' exists
|
163
|
+
# - there's only one sub element named 'size'
|
164
|
+
# - it doesn't have sub elements itself
|
165
|
+
# - and it contains an integer value
|
166
|
+
# Such elements are extraneous for the most part, and are not consistently
|
167
|
+
# located - sometimes they are in the Array-ish elements they reference,
|
168
|
+
# sometimes they are alongside them. In any case they confuse the logic
|
169
|
+
# when deciding if an element with sub-elements should become an
|
170
|
+
# Array or a Hash.
|
171
|
+
#
|
172
|
+
def self.remove_size_sub_elem(elem)
|
173
|
+
size_elems = elem.elements.to_a.select { |subel| subel.name == SIZE_ELEM_NAME }
|
174
|
+
size_elem = size_elems.first
|
175
|
+
return unless size_elem
|
176
|
+
return unless size_elems.count == 1
|
177
|
+
return if size_elem.has_elements?
|
178
|
+
return unless size_elem.text.jss_integer?
|
179
|
+
elem.delete_element size_elem
|
180
|
+
end
|
181
|
+
|
182
|
+
# convert an XML element into an Array
|
183
|
+
def self.elem_as_array(model, elem)
|
184
|
+
remove_size_sub_elem elem
|
185
|
+
arr = []
|
186
|
+
elem.each do |subelem|
|
187
|
+
# Recursion for the win!
|
188
|
+
arr << process_map_item(model, subelem)
|
189
|
+
end # each subelem
|
190
|
+
arr.compact
|
191
|
+
end
|
192
|
+
|
193
|
+
# convert an XML element into a Hash
|
194
|
+
def self.elem_as_hash(model, elem)
|
195
|
+
remove_size_sub_elem elem
|
196
|
+
hsh = {}
|
197
|
+
model.each do |key, mod|
|
198
|
+
val = process_map_item(mod, elem.elements[key.to_s])
|
199
|
+
val = [] if mod.is_a?(Array) && val.to_s.empty?
|
200
|
+
val = {} if mod.is_a?(Hash) && val.to_s.empty?
|
201
|
+
hsh[key] = val
|
202
|
+
end
|
203
|
+
hsh
|
204
|
+
end
|
205
|
+
|
206
|
+
end # module XMLWorkarounds
|
207
|
+
|
208
|
+
end # module
|