ruby-jss 0.11.0 → 0.12.0

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.

data/lib/jss/composer.rb CHANGED
@@ -26,188 +26,165 @@
26
26
  ###
27
27
  module JSS
28
28
 
29
- ###
30
- ### This module provides two methods for building very simple Casper-happy .pkg and .dmg packages for deployment.
31
- ###
32
- ### Unlike Composer.app from JAMF, this module currently doesn't offer a way to do a before/after disk scan
33
- ### and use the differences to build the root folder from which the package is built. Nor does the module support
34
- ### editing the pre/post install scripts in .pkgs.
35
- ###
36
- ### The 'root folder', a folder representing the root filesystem of the target machine where the package will be installed,
37
- ### must already exist and be fully populated and with correct permissions.
38
- ###
39
- module Composer
40
-
41
-
29
+ ###
30
+ ### This module provides two methods for building very simple Casper-happy .pkg and .dmg packages for deployment.
31
+ ###
32
+ ### Unlike Composer.app from JAMF, this module currently doesn't offer a way to do a before/after disk scan
33
+ ### and use the differences to build the root folder from which the package is built. Nor does the module support
34
+ ### editing the pre/post install scripts in .pkgs.
35
+ ###
36
+ ### The 'root folder', a folder representing the root filesystem of the target machine where the package will be installed,
37
+ ### must already exist and be fully populated and with correct permissions.
38
+ ###
39
+ module Composer
42
40
 
43
- #####################################
44
- ### Constants
45
- #####################################
41
+ #####################################
42
+ ### Constants
43
+ #####################################
46
44
 
47
- ### the apple pkgutil tool
48
- PKG_UTIL = Pathname.new "/usr/sbin/pkgutil"
45
+ ### the apple pkgutil tool
46
+ PKG_UTIL = Pathname.new '/usr/sbin/pkgutil'
49
47
 
50
- ### The location of the cli tool for making .pkgs
51
- PKGBUILD = Pathname.new "/usr/bin/pkgbuild"
48
+ ### The location of the cli tool for making .pkgs
49
+ PKGBUILD = Pathname.new '/usr/bin/pkgbuild'
52
50
 
53
- ### the default bundle identifier prefix for pkgs
54
- PKG_BUNDLE_ID_PFX = 'jss_gem_composer'
51
+ ### the default bundle identifier prefix for pkgs
52
+ PKG_BUNDLE_ID_PFX = 'jss_gem_composer'.freeze
55
53
 
56
- ### Apple's hdiutil for making dmgs
57
- HDI_UTIL= '/usr/bin/hdiutil'
54
+ ### Apple's hdiutil for making dmgs
55
+ HDI_UTIL = '/usr/bin/hdiutil'.freeze
58
56
 
59
- ### Where to save the output ?
60
- DEFAULT_OUT_DIR = Pathname.new "/Users/Shared"
57
+ ### Where to save the output ?
58
+ DEFAULT_OUT_DIR = Pathname.new '/Users/Shared'
61
59
 
62
- ###
63
- ### Make a casper-happy .pkg out of a root folder, permissions are assumed to be correct.
64
- ###
65
- ### @param name[String] the name of the .pkg. The .pkg suffix will be added if not present
66
- ###
67
- ### @param version[String] the version of the .pkg, needed for building the .pkg
68
- ###
69
- ### @param root[String, Pathname] the path to the "root folder" representing
70
- ### the root file system of the target install drive
71
- ###
72
- ### @param opts[Hash] the options for building the .pkg
73
- ###
74
- ### @options opts :pkg_id[String] the full package if for the new pkg.
75
- ### e.g. 'com.mycompany.myapp'
76
- ###
77
- ### @option opts :bundle_id_prefix[String] the pkg bundle identifier prefix.
78
- ### If no :pkg_id is provided, one is made using this prefix and
79
- ### the name provided. e.g. 'com.mycompany'
80
- ### Defaults to '{PKG_BUNDLE_ID_PFX}'. See 'man pkgbuild' for more info
81
- ###
82
- ### @option opts :out_dir[String,Pathname] he folder in which the .pkg will be
83
- ### created. Defaults to {DEFAULT_OUT_DIR}
84
- ###
85
- ### @option opts :preserve_ownership[Boolean] If true, the owner/group of the
86
- ### rootpath are preserved.
87
- ### Default is false: they become the pkgbuild/installer "recommended"
88
- ### (root/wheel or root/admin)
89
- ###
90
- ### @option opts :signing_identity[String] the name of the signing identity to
91
- ### use for signing the pkg. See `man pkgbuild` for details
92
- ###
93
- ### @option opts :keychain[String,Pathname] the path to the keychain
94
- ### containing the the signing identity. See `man pkgbuild` for details
95
- ###
96
- ### @option opts :certs[String,Array<String>] the Common Name(s) of additional
97
- ### certificates to include when signing the pkg. See `man pkgbuild` for
98
- ### details
99
- ###
100
- ### @option opts :include_timestamp[Boolean] should a trusted timestamp be
101
- ### included when signing the pkg. See `man pkgbuild` for details.
102
- ### Default depends on the situation, but true is the same as using
103
- ### --timestamp with pkgbuild, and false is --timestamp=none
104
- ###
105
- ### @return [Pathname] the local path to the new .pkg
106
- ###
107
- def self.mk_pkg(name, version, root, opts = {})
108
- raise NoSuchItemError, "Missing pkgbuild tool. Please make sure you're running 10.8 or later." unless PKGBUILD.executable?
109
-
110
- opts[:out_dir] ||= DEFAULT_OUT_DIR
111
- opts[:bundle_id_prefix] ||= PKG_BUNDLE_ID_PFX
112
-
113
- pkg_filename = name.end_with?(".pkg") ? name : name+".pkg"
114
- pkg_id = opts[:pkg_id]
115
- pkg_id ||= opts[:bundle_id_prefix] + "." + name
116
- pkg_out = "#{opts[:out_dir]}/#{pkg_filename}"
117
- pkg_ownership = opts[:preserve_ownership] ? "preserve" : "recommended"
118
-
119
- if opts[:signing_identity]
120
- signing = "--sign #{Shellwords.escape opts[:signing_identity]}"
121
- signing << " --keychain #{Shellwords.escape opts[:keychain].to_s}" if opts[:keychain]
122
- signing << ' --timestamp' if opts[:include_timestamp]
123
- signing << ' --timestamp=none' if opts[:include_timestamp] == false
124
- case opts[:certs]
125
- when Array
126
- opts[:certs].each { |c| signing << " --cert #{Shellwords.escape c}" }
127
- when String
128
- signing << " --cert #{Shellwords.escape opts[:certs]}"
129
- end # case
130
- else
131
- signing = ''
132
- end # if opts[:signing_identity]
133
-
134
- ### first, run 'analyze' to get a 'component plist' in which we can change some settings
135
- ### for any bundles in the root (bundles like .apps, frameworks, plugins, etc..)
136
- ###
137
- ### we edit the settings thus:
138
- ### BundleOverwriteAction = upgrade, totally replace any version current on disk
139
- ### BundleIsVersionChecked = false, allow us to install regardless of what version is currently installed
140
- ### BundleIsRelocatable = false, if there's a version of this in some other location, Do Not move this one there after installation
141
- ### BundleHasStrictIdentifier = false, don't care if there's something at the install path with a different bundle id.
142
- ###
143
- ### In other words, just install the thing!
144
- ### (see 'man pkgbuild' for more info)
145
- ###
146
- ###
147
- comp_plist_out = Pathname.new "/tmp/#{PKG_BUNDLE_ID_PFX}-#{pkg_filename}.plist"
148
- system "#{PKGBUILD} --analyze --root '#{root}' '#{comp_plist_out}'"
149
- comp_plist = Plist.parse_xml comp_plist_out.read
150
-
151
- ### if the plist is empty, there are no bundles in the pkg
152
- if comp_plist[0].nil?
153
- comp_plist_arg = ''
154
- else
155
- ### otherwise, edit the bundle dictionaries
156
- comp_plist.each do |bndl|
157
- bndl.delete "ChildBundles" if bndl["ChildBundles"]
158
- bndl["BundleOverwriteAction"] = "upgrade"
159
- bndl["BundleIsVersionChecked"] = false
160
- bndl["BundleIsRelocatable"] = false
161
- bndl["BundleHasStrictIdentifier"] = false
60
+ ### Make a casper-happy .pkg out of a root folder, permissions are assumed to be correct.
61
+ ###
62
+ ### @param name[String] the name of the .pkg. The .pkg suffix will be added if not present
63
+ ###
64
+ ### @param version[String] the version of the .pkg, needed for building the .pkg
65
+ ###
66
+ ### @param root[String, Pathname] the path to the 'root folder' representing
67
+ ### the root file system of the target install drive
68
+ ###
69
+ ### @param opts[Hash] the options for building the .pkg
70
+ ###
71
+ ### @options opts :pkg_id[String] the full package if for the new pkg.
72
+ ### e.g. 'com.mycompany.myapp'
73
+ ###
74
+ ### @option opts :bundle_id_prefix[String] the pkg bundle identifier prefix.
75
+ ### If no :pkg_id is provided, one is made using this prefix and
76
+ ### the name provided. e.g. 'com.mycompany'
77
+ ### Defaults to '{PKG_BUNDLE_ID_PFX}'. See 'man pkgbuild' for more info
78
+ ###
79
+ ### @option opts :out_dir[String,Pathname] he folder in which the .pkg will be
80
+ ### created. Defaults to {DEFAULT_OUT_DIR}
81
+ ###
82
+ ### @option opts :preserve_ownership[Boolean] If true, the owner/group of the
83
+ ### rootpath are preserved.
84
+ ### Default is false: they become the pkgbuild/installer 'recommended'
85
+ ### (root/wheel or root/admin)
86
+ ###
87
+ ### @option opts :signing_identity[String] the optional name of the signing identity (certificate) to
88
+ ### use for signing the pkg. See `man pkgbuild` for details
89
+ ###
90
+ ### @option opts :signing_options[String] the optional string of options to pass to pkgbuild.
91
+ ### See `man pkgbuild` for details
92
+ ###
93
+ ### @return [Pathname] the local path to the new .pkg
94
+ ###
95
+ def self.mk_pkg(name, version, root, opts = {})
96
+ raise NoSuchItemError, "Missing pkgbuild tool. Please make sure you're running 10.8 or later." unless PKGBUILD.executable?
97
+
98
+ opts[:out_dir] ||= DEFAULT_OUT_DIR
99
+ opts[:bundle_id_prefix] ||= PKG_BUNDLE_ID_PFX
100
+
101
+ pkg_filename = name.end_with?('.pkg') ? name : name + '.pkg'
102
+ pkg_id = opts[:pkg_id]
103
+ pkg_id ||= opts[:bundle_id_prefix] + '.' + name
104
+ pkg_out = "#{opts[:out_dir]}/#{pkg_filename}"
105
+ pkg_ownership = opts[:preserve_ownership] ? 'preserve' : 'recommended'
106
+
107
+ if opts[:signing_identity]
108
+ signing = "--sign '#{opts[:signing_identity]}'"
109
+ signing << " #{opts[:signing_options]}" if opts[:signing_options]
110
+ else
111
+ signing = ''
112
+ end # if opts[:signing_identity]
113
+
114
+ ### first, run 'analyze' to get a 'component plist' in which we can change some settings
115
+ ### for any bundles in the root (bundles like .apps, frameworks, plugins, etc..)
116
+ ###
117
+ ### we edit the settings thus:
118
+ ### BundleOverwriteAction = upgrade, totally replace any version current on disk
119
+ ### BundleIsVersionChecked = false, allow us to install regardless of what version is currently installed
120
+ ### BundleIsRelocatable = false, if there's a version of this in some other location, Do Not move this one there after installation
121
+ ### BundleHasStrictIdentifier = false, don't care if there's something at the install path with a different bundle id.
122
+ ###
123
+ ### In other words, just install the thing!
124
+ ### (see 'man pkgbuild' for more info)
125
+ ###
126
+ ###
127
+ comp_plist_out = Pathname.new "/tmp/#{PKG_BUNDLE_ID_PFX}-#{pkg_filename}.plist"
128
+ system "#{PKGBUILD} --analyze --root '#{root}' '#{comp_plist_out}'"
129
+ comp_plist = Plist.parse_xml comp_plist_out.read
130
+
131
+ ### if the plist is empty, there are no bundles in the pkg
132
+ if comp_plist[0].nil?
133
+ comp_plist_arg = ''
134
+ else
135
+ ### otherwise, edit the bundle dictionaries
136
+ comp_plist.each do |bndl|
137
+ bndl.delete 'ChildBundles' if bndl['ChildBundles']
138
+ bndl['BundleOverwriteAction'] = 'upgrade'
139
+ bndl['BundleIsVersionChecked'] = false
140
+ bndl['BundleIsRelocatable'] = false
141
+ bndl['BundleHasStrictIdentifier'] = false
142
+ end
143
+ ### write out the edits
144
+ comp_plist_out.open('w') { |f| f.write comp_plist.to_plist }
145
+ comp_plist_arg = "--component-plist '#{comp_plist_out}'"
162
146
  end
163
- ### write out the edits
164
- comp_plist_out.open('w'){|f| f.write comp_plist.to_plist}
165
- comp_plist_arg = "--component-plist '#{comp_plist_out}'"
166
- end
167
147
 
168
- ### now build the pkg
169
- begin
170
- it_built = system "#{PKGBUILD} --identifier '#{pkg_id}' --version '#{version}' --ownership #{pkg_ownership} --install-location / --root '#{root}' #{signing} #{comp_plist_arg} '#{pkg_out}' "
148
+ ### now build the pkg
149
+ begin
150
+ it_built = system "#{PKGBUILD} --identifier '#{pkg_id}' --version '#{version}' --ownership #{pkg_ownership} --install-location / --root '#{root}' #{signing} #{comp_plist_arg} '#{pkg_out}' "
171
151
 
172
- raise RuntimeError, "There was an error building the .pkg" unless it_built
173
- ensure
174
- comp_plist_out.delete if comp_plist_out.exist?
175
- end
176
-
177
- return Pathname.new pkg_out
178
- end # mk_dot_pkg
179
-
180
-
181
- ###
182
- ### Make a casper-happy .dmg out of a root folder, permissions are assumed to be correct.
183
- ###
184
- ### @param name[String] The name of the .dmg, the suffix will be added if needed
185
- ###
186
- ### @param root[String, Pathname] the path to the "root folder" representing the root file system of the target install drive
187
- ###
188
- ### @param out_dir[String, Pathname] the folder in which the .pkg will be created. Defaults to {DEFAULT_OUT_DIR}
189
- ###
190
- ### @return [Pathname] the local path to the new .dmg
191
- ###
192
- ###
193
- def self.mk_dmg(name, root, out_dir = DEFAULT_OUT_DIR)
152
+ raise 'There was an error building the .pkg' unless it_built
153
+ ensure
154
+ comp_plist_out.delete if comp_plist_out.exist?
155
+ end
194
156
 
195
- dmg_filename = "#{name}.dmg"
196
- dmg_vol = name
197
- dmg_out = Pathname.new "#{out_dir}/#{dmg_filename}"
198
- if dmg_out.exist?
199
- mv_to = dmg_out.dirname + "#{dmg_out.basename}.#{Time.now.strftime('%Y%m%d%H%M%S')}"
200
- dmg_out.rename mv_to
201
- end # if dmg out exist
157
+ Pathname.new pkg_out
158
+ end # mk_dot_pkg
202
159
 
203
- ### TODO - this may need to be sudo'd to handle proper internal permissions.
204
- system "#{HDI_UTIL} create -volname '#{dmg_vol}' -scrub -srcfolder '#{root}' '#{dmg_out}'"
160
+ ###
161
+ ### Make a casper-happy .dmg out of a root folder, permissions are assumed to be correct.
162
+ ###
163
+ ### @param name[String] The name of the .dmg, the suffix will be added if needed
164
+ ###
165
+ ### @param root[String, Pathname] the path to the "root folder" representing the root file system of the target install drive
166
+ ###
167
+ ### @param out_dir[String, Pathname] the folder in which the .pkg will be created. Defaults to {DEFAULT_OUT_DIR}
168
+ ###
169
+ ### @return [Pathname] the local path to the new .dmg
170
+ ###
171
+ ###
172
+ def self.mk_dmg(name, root, out_dir = DEFAULT_OUT_DIR)
173
+ dmg_filename = "#{name}.dmg"
174
+ dmg_vol = name
175
+ dmg_out = Pathname.new "#{out_dir}/#{dmg_filename}"
176
+ if dmg_out.exist?
177
+ mv_to = dmg_out.dirname + "#{dmg_out.basename}.#{Time.now.strftime('%Y%m%d%H%M%S')}"
178
+ dmg_out.rename mv_to
179
+ end # if dmg out exist
205
180
 
206
- raise RuntimeError, "There was an error building the .dmg" unless $?.exitstatus == 0
207
- return Pathname.new dmg_out
181
+ ### TODO - this may need to be sudo'd to handle proper internal permissions.
182
+ system "#{HDI_UTIL} create -volname '#{dmg_vol}' -scrub -srcfolder '#{root}' '#{dmg_out}'"
208
183
 
209
- end # mk_dmg
184
+ raise 'There was an error building the .dmg' unless $?.exitstatus.zero?
185
+ Pathname.new dmg_out
186
+ end # mk_dmg
210
187
 
188
+ end # module Composer
211
189
 
212
- end # module Composer
213
190
  end # module JSS
@@ -1,80 +1,135 @@
1
- ### Copyright 2018 Pixar
1
+ # Copyright 2018 Pixar
2
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
- ###
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
25
 
26
- ###
26
+ #
27
27
  class Hash
28
28
 
29
- ###
30
- ### Convert Hash values to nil.
31
- ###
32
- ### With no block, values equalling the String, or any member of the Array, given
33
- ### will be converted to nil. Equality is evaluated with == and Array#include?
34
- ###
35
- ### With a block, if the result of the block evaluates to true, the value is converted to nil.
36
- ###
37
- ### Subhashes are ignored unless recurse is true.
38
- ###
39
- ### @param to_nils[String,Array] Hash values equal to (==) these become nil. Defaults to empty string
40
- ###
41
- ### @param recurse[Boolean] should sub-Hashes be nillified?
42
- ###
43
- ### @yield [value] Hash values for which the block returns true will become nil.
44
- ###
45
- ### @return [Hash] the hash with the desired values converted to nil
46
- ###
47
- ### @example
48
- ### hash = {:foo => '', :bar => {:baz => '' }}
49
- ### hash.jss_nillify! # {:foo => nil, :bar => {:baz => '' }}
50
- ###
51
- ### hash = {:foo => '', :bar => {:baz => '' }}
52
- ### hash.jss_nillify! '', :recurse # {:foo => nil, :bar => {:baz => nil }}
53
- ###
54
- ### hash = {:foo => 123, :bar => {:baz => '', :bim => "123" }}
55
- ### hash.jss_nillify! ['', 123], :recurse # {:foo => nil, :bar => {:baz => nil, :bim => "123" }}
56
- ###
57
- ### hash = {:foo => 123, :bar => {:baz => '', :bim => "123" }}
58
- ### hash.jss_nillify!(:anything, :recurse){|v| v.to_i == 123 } # {:foo => nil, :bar => {:baz => '', :bim => nil }}
59
- ###
60
- def jss_nillify!(to_nils = '', recurse = false, &block )
61
-
29
+ #
30
+ # Convert Hash values to nil.
31
+ #
32
+ # With no block, values equalling the String, or any member of the Array, given
33
+ # will be converted to nil. Equality is evaluated with == and Array#include?
34
+ #
35
+ # With a block, if the result of the block evaluates to true, the value is converted to nil.
36
+ #
37
+ # Subhashes are ignored unless recurse is true.
38
+ #
39
+ # @param to_nils[String,Array] Hash values equal to (==) these become nil. Defaults to empty string
40
+ #
41
+ # @param recurse[Boolean] should sub-Hashes be nillified?
42
+ #
43
+ # @yield [value] Hash values for which the block returns true will become nil.
44
+ #
45
+ # @return [Hash] the hash with the desired values converted to nil
46
+ #
47
+ # @example
48
+ # hash = {:foo => '', :bar => {:baz => '' }}
49
+ # hash.jss_nillify! # {:foo => nil, :bar => {:baz => '' }}
50
+ #
51
+ # hash = {:foo => '', :bar => {:baz => '' }}
52
+ # hash.jss_nillify! '', :recurse # {:foo => nil, :bar => {:baz => nil }}
53
+ #
54
+ # hash = {:foo => 123, :bar => {:baz => '', :bim => "123" }}
55
+ # hash.jss_nillify! ['', 123], :recurse # {:foo => nil, :bar => {:baz => nil, :bim => "123" }}
56
+ #
57
+ # hash = {:foo => 123, :bar => {:baz => '', :bim => "123" }}
58
+ # hash.jss_nillify!(:anything, :recurse){|v| v.to_i == 123 } # {:foo => nil, :bar => {:baz => '', :bim => nil }}
59
+ #
60
+ def jss_nillify!(to_nils = '', recurse = false, &block)
62
61
  nillify_these = [] << to_nils
63
62
  nillify_these.flatten!
64
63
 
65
- self.each_pair do |k,v|
64
+ each_pair do |k, v|
66
65
  if v.class == Hash
67
66
  v.jss_nillify!(to_nils, recurse, &block)
68
67
  next
69
68
  end
70
- do_it = if block_given?
71
- yield v
72
- else
73
- nillify_these.include? v
74
- end
69
+ do_it =
70
+ if block_given?
71
+ yield v
72
+ else
73
+ nillify_these.include? v
74
+ end
75
75
  self[k] = nil if do_it
76
76
  end # each pair
77
77
  end # def nillify
78
- end # class
79
78
 
79
+ # Since a lot of JSON data from the API comes as deeply-nested structures
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 Class attributes.
82
+ #
83
+ # For example see the 'hardware' subset of a JSS::Computer's API data,
84
+ # which is stored as a Hash in the {JSS::Computer.hardware} attribute.
85
+ #
86
+ # To refer to the percent-full value of one of the machine's drives, you need
87
+ # to use e.g. this:
88
+ #
89
+ # computer_instance.hardware[:storage].first[:partition][:percentage_full]
90
+ #
91
+ # It would be nice to use method-like chains to access that data,
92
+ # similar to what OpenStruct provides.
93
+ #
94
+ # But, there are two problems with just storing #hardware as an OpenStruct:
95
+ # 1) we'd lose some important Hash methods, like #keys and #values, breaking
96
+ # backward compatibility. 2) OpenStructs only work on the Hash itself, not
97
+ # not it's contents.
98
+ #
99
+ # So to get the best of both worlds, we use the RecursiveOpenStruct gem
100
+ #
101
+ # https://github.com/aetherknight/recursive-open-struct
102
+ #
103
+ # which subclasses OpenStruct to be recursive.
104
+ #
105
+ # And, instead of replacing the Hash, we'll add a RecursiveOpenStruct version
106
+ # of itself to itself as an attribute.
107
+ #
108
+ # Now, we can access the same data using this:
109
+ #
110
+ # computer_instance.hardware.jss_ros.storage.first.partition.percentage_full
111
+ #
112
+ # CAVEAT: Treat these as read-only.
113
+ #
114
+ # While the Hashes themselves may be mutable, their use in ruby-jss Classes
115
+ # should be usually be considered read-only - and the RecursiveOpenStruct
116
+ # object created by this method should not be changed. Changes to the Hash
117
+ # or the RecursiveOpenStruct are NOT synced between them
118
+ #
119
+ # This should be fine for the intended uses. Data like Computer#hardware
120
+ # isn't sent back to the JSS via Computer#update, since it must come
121
+ # from a 'recon' anyway. Data that is sent back to the JSS will have
122
+ # setter methods defined in the class or a mixin module (e.g. the
123
+ # Locatable module).
124
+ #
125
+ # Since the data is read-only, why not use the ImmutableStruct gem, used
126
+ # elsewhere in ruby-jss? Because ImmutableStruct is really for creating
127
+ # fully-fleshed-out read-only classes, with a known set of attributes rather
128
+ # than just giving us a nicer way to access Hash data with arbitrary keys.
129
+ #
130
+ def jss_recursive_ostruct
131
+ @jss_ros ||= RecursiveOpenStruct.new(self, recurse_over_arrays: true)
132
+ end
133
+ alias jss_ros jss_recursive_ostruct
80
134
 
135
+ end # class