ruby-jss 0.11.0 → 0.12.0

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.

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