ruby-jss 0.6.5 → 0.6.6
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.
- checksums.yaml +4 -4
- data/CHANGES.md +57 -5
- data/lib/jss.rb +78 -94
- data/lib/jss/api_connection.rb +8 -0
- data/lib/jss/api_object.rb +126 -102
- data/lib/jss/api_object/creatable.rb +33 -15
- data/lib/jss/api_object/distribution_point.rb +4 -1
- data/lib/jss/api_object/extension_attribute.rb +1 -1
- data/lib/jss/api_object/package.rb +121 -187
- data/lib/jss/api_object/policy.rb +590 -251
- data/lib/jss/api_object/script.rb +92 -128
- data/lib/jss/api_object/self_servable.rb +93 -117
- data/lib/jss/api_object/updatable.rb +12 -27
- data/lib/jss/api_object/uploadable.rb +12 -15
- data/lib/jss/client.rb +178 -156
- data/lib/jss/db_connection.rb +34 -49
- data/lib/jss/ruby_extensions/string.rb +25 -21
- data/lib/jss/version.rb +1 -1
- data/lib/jss/webhooks.rb +52 -0
- data/lib/jss/webhooks/README.md +269 -0
- data/lib/jss/webhooks/configuration.rb +212 -0
- data/lib/jss/webhooks/data/sample_handlers/RestAPIOperation-executable +90 -0
- data/lib/jss/webhooks/data/sample_handlers/RestAPIOperation.rb +44 -0
- data/lib/jss/webhooks/data/sample_jsons/ComputerAdded.json +27 -0
- data/lib/jss/webhooks/data/sample_jsons/ComputerCheckIn.json +27 -0
- data/lib/jss/webhooks/data/sample_jsons/ComputerInventoryCompleted.json +27 -0
- data/lib/jss/webhooks/data/sample_jsons/ComputerPolicyFinished.json +27 -0
- data/lib/jss/webhooks/data/sample_jsons/ComputerPushCapabilityChanged.json +27 -0
- data/lib/jss/webhooks/data/sample_jsons/JSSShutdown.json +14 -0
- data/lib/jss/webhooks/data/sample_jsons/JSSStartup.json +14 -0
- data/lib/jss/webhooks/data/sample_jsons/MobileDeviceCheckIn.json +26 -0
- data/lib/jss/webhooks/data/sample_jsons/MobileDeviceCommandCompleted.json +26 -0
- data/lib/jss/webhooks/data/sample_jsons/MobileDeviceEnrolled.json +26 -0
- data/lib/jss/webhooks/data/sample_jsons/MobileDevicePushSent.json +26 -0
- data/lib/jss/webhooks/data/sample_jsons/MobileDeviceUnEnrolled.json +26 -0
- data/lib/jss/webhooks/data/sample_jsons/PatchSoftwareTitleUpdated.json +14 -0
- data/lib/jss/webhooks/data/sample_jsons/PushSent.json +11 -0
- data/lib/jss/webhooks/data/sample_jsons/RestAPIOperation.json +15 -0
- data/lib/jss/webhooks/data/sample_jsons/SCEPChallenge.json +10 -0
- data/lib/jss/webhooks/data/sample_jsons/SmartGroupComputerMembershipChange.json +13 -0
- data/lib/jss/webhooks/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +13 -0
- data/lib/jss/webhooks/event.rb +138 -0
- data/lib/jss/webhooks/event/computer_added.rb +37 -0
- data/lib/jss/webhooks/event/computer_check_in.rb +37 -0
- data/lib/jss/webhooks/event/computer_inventory_completed.rb +37 -0
- data/lib/jss/webhooks/event/computer_policy_finished.rb +37 -0
- data/lib/jss/webhooks/event/computer_push_capability_changed.rb +37 -0
- data/lib/jss/webhooks/event/handlers.rb +191 -0
- data/lib/jss/webhooks/event/jss_shutdown.rb +37 -0
- data/lib/jss/webhooks/event/jss_startup.rb +37 -0
- data/lib/jss/webhooks/event/mobile_device_check_in.rb +37 -0
- data/lib/jss/webhooks/event/mobile_device_command_completed.rb +37 -0
- data/lib/jss/webhooks/event/mobile_device_enrolled.rb +37 -0
- data/lib/jss/webhooks/event/mobile_device_push_sent.rb +37 -0
- data/lib/jss/webhooks/event/mobile_device_unenrolled.rb +37 -0
- data/lib/jss/webhooks/event/patch_software_title_updated.rb +37 -0
- data/lib/jss/webhooks/event/push_sent.rb +37 -0
- data/lib/jss/webhooks/event/rest_api_operation.rb +37 -0
- data/lib/jss/webhooks/event/scep_challenge.rb +37 -0
- data/lib/jss/webhooks/event/smart_group_computer_membership_change.rb +37 -0
- data/lib/jss/webhooks/event/smart_group_mobile_device_membership_change.rb +37 -0
- data/lib/jss/webhooks/event/webhook.rb +39 -0
- data/lib/jss/webhooks/event_objects.rb +111 -0
- data/lib/jss/webhooks/event_objects/computer.rb +48 -0
- data/lib/jss/webhooks/event_objects/jss.rb +35 -0
- data/lib/jss/webhooks/event_objects/mobile_device.rb +47 -0
- data/lib/jss/webhooks/event_objects/patch_software_title_update.rb +37 -0
- data/lib/jss/webhooks/event_objects/push.rb +32 -0
- data/lib/jss/webhooks/event_objects/rest_api_operation.rb +36 -0
- data/lib/jss/webhooks/event_objects/scep_challenge.rb +31 -0
- data/lib/jss/webhooks/event_objects/smart_group.rb +34 -0
- data/lib/jss/webhooks/server_app.rb +36 -0
- data/lib/jss/webhooks/server_app/routes.rb +26 -0
- data/lib/jss/webhooks/server_app/routes/handle_webhook_event.rb +38 -0
- data/lib/jss/webhooks/server_app/routes/home.rb +36 -0
- data/lib/jss/webhooks/server_app/self_signed_cert.rb +64 -0
- data/lib/jss/webhooks/server_app/server.rb +59 -0
- data/lib/jss/webhooks/version.rb +31 -0
- metadata +63 -4
@@ -25,20 +25,15 @@
|
|
25
25
|
###
|
26
26
|
module JSS
|
27
27
|
|
28
|
-
#####################################
|
29
28
|
### Module Variables
|
30
29
|
#####################################
|
31
30
|
|
32
|
-
|
33
|
-
#####################################
|
34
31
|
### Module Methods
|
35
32
|
#####################################
|
36
33
|
|
37
|
-
#####################################
|
38
34
|
### Sub-Modules
|
39
35
|
#####################################
|
40
36
|
|
41
|
-
###
|
42
37
|
### A mix-in module that allows objects to be created in the JSS
|
43
38
|
### via the API.
|
44
39
|
###
|
@@ -63,35 +58,58 @@ module JSS
|
|
63
58
|
###
|
64
59
|
module Creatable
|
65
60
|
|
66
|
-
#####################################
|
67
61
|
### Constants
|
68
62
|
#####################################
|
69
63
|
|
70
64
|
CREATABLE = true
|
71
65
|
|
72
|
-
#####################################
|
73
66
|
### Variables
|
74
67
|
#####################################
|
75
68
|
|
76
|
-
#####################################
|
77
69
|
### Mixed-in Instance Methods
|
78
70
|
#####################################
|
79
71
|
|
80
|
-
###
|
81
72
|
### Create a new object in the JSS.
|
82
73
|
###
|
83
74
|
### @return [Integer] the jss ID of the newly created object
|
84
75
|
###
|
85
76
|
def create
|
86
|
-
raise JSS::UnsupportedError, "Creating or editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless
|
77
|
+
raise JSS::UnsupportedError, "Creating or editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless respond_to? :create
|
87
78
|
raise AlreadyExistsError, "This #{self.class::RSRC_OBJECT_KEY} already exists. Use #update to make changes." if @in_jss
|
88
|
-
JSS::API.post_rsrc(
|
89
|
-
|
90
|
-
@id = $1.to_i
|
79
|
+
JSS::API.post_rsrc(@rest_rsrc, rest_xml) =~ %r{><id>(\d+)</id><}
|
80
|
+
@id = Regexp.last_match(1).to_i
|
91
81
|
@in_jss = true
|
92
82
|
@need_to_update = false
|
93
|
-
@rest_rsrc =
|
94
|
-
|
83
|
+
@rest_rsrc = "#{self.class::RSRC_BASE}/id/#{@id}"
|
84
|
+
@id
|
85
|
+
end
|
86
|
+
|
87
|
+
### make a clone of this API object, with a new name. The class must be creatable
|
88
|
+
###
|
89
|
+
### @param name [String] the name for the new object
|
90
|
+
###
|
91
|
+
### @return [APIObject] An uncreated clone of this APIObject with the given name
|
92
|
+
###
|
93
|
+
def clone(new_name)
|
94
|
+
raise JSS::UnsupportedError, 'This class is not creatable in via ruby-jss' unless respond_to? :create
|
95
|
+
raise JSS::AlreadyExistsError, "A #{self.class::RSRC_OBJECT_KEY} already exists with that name" if \
|
96
|
+
self.class.all_names.include? new_name
|
97
|
+
|
98
|
+
orig_in_jss = @in_jss
|
99
|
+
@in_jss = false
|
100
|
+
orig_id = @id
|
101
|
+
@id = nil
|
102
|
+
orig_rsrc = @rest_rsrc
|
103
|
+
@rest_rsrc = "#{self.class::RSRC_BASE}/name/#{CGI.escape new_name}"
|
104
|
+
|
105
|
+
new_obj = dup
|
106
|
+
|
107
|
+
@in_jss = orig_in_jss
|
108
|
+
@id = orig_id
|
109
|
+
@rest_rsrc = orig_rsrc
|
110
|
+
new_obj.name = new_name
|
111
|
+
|
112
|
+
new_obj
|
95
113
|
end
|
96
114
|
|
97
115
|
end # module Creatable
|
@@ -90,7 +90,10 @@ module JSS
|
|
90
90
|
### An empty SHA256 digest
|
91
91
|
EMPTY_PW_256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
92
92
|
|
93
|
+
### Set default local mount for distribution point
|
94
|
+
DEFAULT_MOUNTPOINT_DIR = Pathname.new "/tmp"
|
93
95
|
|
96
|
+
DEFAULT_MOUNTPOINT_PREFIX = "CasperDistribution-id"
|
94
97
|
|
95
98
|
#####################################
|
96
99
|
### Class Variables
|
@@ -317,7 +320,7 @@ module JSS
|
|
317
320
|
|
318
321
|
|
319
322
|
### if we mount for fileservice, where's the mountpoint?
|
320
|
-
@mountpoint = Pathname.new "
|
323
|
+
@mountpoint = Pathname.new "/#{DEFAULT_MOUNTPOINT_DIR}/#{DEFAULT_MOUNTPOINT_PREFIX}#{@id}"
|
321
324
|
|
322
325
|
end #init
|
323
326
|
|
@@ -167,7 +167,7 @@ module JSS
|
|
167
167
|
|
168
168
|
### the name of the EA might have spaces and caps, which the will come to us as symbols with the spaces
|
169
169
|
### as underscores, like this.
|
170
|
-
@symbolized_name = @name.gsub(
|
170
|
+
@symbolized_name = @name.gsub(/-| /,'_').to_sym
|
171
171
|
|
172
172
|
end # init
|
173
173
|
|
@@ -25,50 +25,39 @@
|
|
25
25
|
###
|
26
26
|
module JSS
|
27
27
|
|
28
|
-
#####################################
|
29
28
|
### Module Constants
|
30
29
|
#####################################
|
31
30
|
|
32
|
-
#####################################
|
33
31
|
### Module Variables
|
34
32
|
#####################################
|
35
33
|
|
36
|
-
#####################################
|
37
34
|
### Module Methods
|
38
35
|
#####################################
|
39
36
|
|
40
|
-
#####################################
|
41
37
|
### Classes
|
42
38
|
#####################################
|
43
39
|
|
44
|
-
###
|
45
40
|
### A Package in the JSS
|
46
41
|
###
|
47
42
|
### Also the API provides no access to the package's
|
48
43
|
### file list (index), so indexing must be done separately (usually via Casper Admin)
|
49
44
|
###
|
50
|
-
|
51
|
-
### @see JSS::APIObject
|
52
|
-
###
|
53
|
-
class Package < JSS::APIObject
|
45
|
+
class Package < JSS::APIObject
|
54
46
|
|
55
|
-
#####################################
|
56
47
|
### Mix-Ins
|
57
48
|
#####################################
|
58
49
|
|
59
50
|
include JSS::Creatable
|
60
51
|
include JSS::Updatable
|
61
52
|
|
62
|
-
#####################################
|
63
53
|
### Class Methods
|
64
54
|
#####################################
|
65
55
|
|
66
|
-
#####################################
|
67
56
|
### Class Constants
|
68
57
|
#####################################
|
69
58
|
|
70
59
|
### The base for REST resources of this class
|
71
|
-
RSRC_BASE =
|
60
|
+
RSRC_BASE = 'packages'.freeze
|
72
61
|
|
73
62
|
### the hash key used for the JSON list output of all objects in the JSS
|
74
63
|
RSRC_LIST_KEY = :packages
|
@@ -78,13 +67,13 @@ module JSS
|
|
78
67
|
RSRC_OBJECT_KEY = :package
|
79
68
|
|
80
69
|
### these keys, as well as :id and :name, are present in valid API JSON data for this class
|
81
|
-
VALID_DATA_KEYS = [:fill_existing_users, :fill_user_template, :reboot_required
|
70
|
+
VALID_DATA_KEYS = [:fill_existing_users, :fill_user_template, :reboot_required].freeze
|
82
71
|
|
83
72
|
### The pkg storage folder on the distribution point
|
84
|
-
DIST_POINT_PKGS_FOLDER =
|
73
|
+
DIST_POINT_PKGS_FOLDER = 'Packages'.freeze
|
85
74
|
|
86
75
|
### The possible values for cpu_type (required_processor) in a JSS package
|
87
|
-
CPU_TYPES =
|
76
|
+
CPU_TYPES = %w(None x86 ppc).freeze
|
88
77
|
|
89
78
|
### the possible priorities
|
90
79
|
PRIORITIES = (1..20)
|
@@ -93,23 +82,20 @@ module JSS
|
|
93
82
|
DEFAULT_PRIORITY = 10
|
94
83
|
|
95
84
|
### by default, no processor requirement
|
96
|
-
DEFAULT_PROCESSOR =
|
85
|
+
DEFAULT_PROCESSOR = 'None'.freeze
|
97
86
|
|
98
87
|
### When we shouldn't install anything (e.g. switch w/package)
|
99
|
-
DO_NOT_INSTALL =
|
88
|
+
DO_NOT_INSTALL = 'Do Not Install'.freeze
|
100
89
|
|
101
90
|
### The table in the database for this object
|
102
|
-
DB_TABLE =
|
91
|
+
DB_TABLE = 'packages'.freeze
|
103
92
|
|
104
|
-
#####################################
|
105
93
|
### Class Variables
|
106
94
|
#####################################
|
107
95
|
|
108
|
-
#####################################
|
109
96
|
### Class Methods
|
110
97
|
#####################################
|
111
98
|
|
112
|
-
#####################################
|
113
99
|
### Attributes
|
114
100
|
#####################################
|
115
101
|
|
@@ -161,19 +147,16 @@ module JSS
|
|
161
147
|
### @return [Boolean] does this pkg cause a notification to be sent on self-heal?
|
162
148
|
attr_reader :send_notification
|
163
149
|
|
164
|
-
|
165
|
-
###
|
166
150
|
### @see JSS::APIObject#initialize
|
167
151
|
###
|
168
|
-
def initialize
|
169
|
-
|
152
|
+
def initialize(args = {})
|
170
153
|
super
|
171
154
|
|
172
155
|
### now we have pkg_data with something in it, so fill out the instance vars
|
173
156
|
@allow_uninstalled = @init_data[:allow_uninstalled]
|
174
157
|
@boot_volume_required = @init_data[:boot_volume_required]
|
175
158
|
@category = JSS::APIObject.get_name(@init_data[:category])
|
176
|
-
@category = nil if @category.to_s.casecmp(
|
159
|
+
@category = nil if @category.to_s.casecmp('No category assigned').zero?
|
177
160
|
@filename = @init_data[:filename] || @init_data[:name]
|
178
161
|
@fill_existing_users = @init_data[:fill_existing_users]
|
179
162
|
@fill_user_template = @init_data[:fill_user_template]
|
@@ -186,7 +169,7 @@ module JSS
|
|
186
169
|
@priority = @init_data[:priority] || DEFAULT_PRIORITY
|
187
170
|
@reboot_required = @init_data[:reboot_required]
|
188
171
|
@required_processor = @init_data[:required_processor] || DEFAULT_PROCESSOR
|
189
|
-
@required_processor = nil if @required_processor.to_s.casecmp('none')
|
172
|
+
@required_processor = nil if @required_processor.to_s.casecmp('none').zero?
|
190
173
|
@send_notification = @init_data[:send_notification]
|
191
174
|
@switch_with_package = @init_data[:switch_with_package] || DO_NOT_INSTALL
|
192
175
|
|
@@ -194,9 +177,6 @@ module JSS
|
|
194
177
|
@receipt = @filename ? (JSS::Client::RECEIPTS_FOLDER + @filename.to_s.sub(/.zip$/, '')) : nil
|
195
178
|
end # init
|
196
179
|
|
197
|
-
|
198
|
-
|
199
|
-
###
|
200
180
|
### Change the 'allow to be uninstalled' field in the JSS
|
201
181
|
### NOTE The package must be indexed before this works. Right now, that means
|
202
182
|
### using CasperAdmin.app
|
@@ -205,7 +185,7 @@ module JSS
|
|
205
185
|
###
|
206
186
|
### @return [void]
|
207
187
|
###
|
208
|
-
def allow_uninstalled=
|
188
|
+
def allow_uninstalled=(new_val)
|
209
189
|
return nil if new_val == @allow_uninstalled
|
210
190
|
|
211
191
|
### removable? defaults to false
|
@@ -215,13 +195,10 @@ module JSS
|
|
215
195
|
new_val = false if new_val.to_s.empty?
|
216
196
|
raise JSS::InvalidDataError, "allow_uninstalled must be boolean 'true' or 'false'" unless JSS::TRUE_FALSE.include? new_val
|
217
197
|
|
218
|
-
@allow_uninstalled= new_val
|
198
|
+
@allow_uninstalled = new_val
|
219
199
|
@need_to_update = true
|
220
|
-
|
221
200
|
end
|
222
201
|
|
223
|
-
|
224
|
-
###
|
225
202
|
### Change the boot volume required field in the JSS
|
226
203
|
###
|
227
204
|
### @param new_val[Boolean]
|
@@ -231,20 +208,18 @@ module JSS
|
|
231
208
|
def boot_volume_required=(new_val)
|
232
209
|
return nil if new_val == @boot_volume_required
|
233
210
|
new_val = false if new_val.to_s.empty?
|
234
|
-
raise JSS::InvalidDataError,
|
211
|
+
raise JSS::InvalidDataError, 'install_if_reported_available must be boolean true or false' unless JSS::TRUE_FALSE.include? new_val
|
235
212
|
@boot_volume_required = new_val
|
236
213
|
@need_to_update = true
|
237
214
|
end
|
238
215
|
|
239
|
-
|
240
|
-
###
|
241
216
|
### Change the category in the JSS
|
242
217
|
###
|
243
218
|
### @param new_val[String] must be one listed by 'JSS::Category.all_names'
|
244
219
|
###
|
245
220
|
### @return [void]
|
246
221
|
###
|
247
|
-
def category=
|
222
|
+
def category=(new_val)
|
248
223
|
return nil if new_val == @category
|
249
224
|
new_val = nil if new_val == ''
|
250
225
|
new_val ||= JSS::Category::DEFAULT_CATEGORY
|
@@ -253,7 +228,6 @@ module JSS
|
|
253
228
|
@need_to_update = true
|
254
229
|
end
|
255
230
|
|
256
|
-
###
|
257
231
|
### Change the package filename.
|
258
232
|
### Setting it to nil or empty will make it match the display name
|
259
233
|
###
|
@@ -261,24 +235,22 @@ module JSS
|
|
261
235
|
###
|
262
236
|
### @return [void]
|
263
237
|
###
|
264
|
-
def filename=
|
238
|
+
def filename=(new_val)
|
265
239
|
new_val = nil if new_val == ''
|
266
240
|
new_val ||= @name
|
267
241
|
return nil if new_val == @filename
|
268
|
-
$stderr.puts
|
242
|
+
$stderr.puts 'WARNING: you must change the filename on the master Distribution Point. See JSS::Package.update_master_filename.' if @in_jss
|
269
243
|
@filename = new_val
|
270
244
|
@need_to_update = true
|
271
245
|
end
|
272
246
|
|
273
|
-
|
274
|
-
###
|
275
247
|
### Change the Fill Existing Users value
|
276
248
|
###
|
277
249
|
### @param new_val[Boolean]
|
278
250
|
###
|
279
251
|
### @return [void]
|
280
252
|
###
|
281
|
-
def fill_existing_users=
|
253
|
+
def fill_existing_users=(new_val)
|
282
254
|
return nil if new_val == @fill_existing_users
|
283
255
|
new_val = false if new_val.to_s.empty?
|
284
256
|
raise JSS::InvalidDataError, "fill_existing_users must be boolean 'true' or 'false'" unless JSS::TRUE_FALSE.include? new_val
|
@@ -286,14 +258,13 @@ module JSS
|
|
286
258
|
@need_to_update = true
|
287
259
|
end
|
288
260
|
|
289
|
-
###
|
290
261
|
### Change the fill_user_template value
|
291
262
|
###
|
292
263
|
### @param new_val[Boolean]
|
293
264
|
###
|
294
265
|
### @return [void]
|
295
266
|
###
|
296
|
-
def fill_user_template=
|
267
|
+
def fill_user_template=(new_val)
|
297
268
|
return nil if new_val == @fill_user_template
|
298
269
|
new_val = false if new_val.to_s.empty?
|
299
270
|
raise JSS::InvalidDataError, "fill_user_template must be boolean 'true' or 'false'" unless JSS::TRUE_FALSE.include? new_val
|
@@ -301,57 +272,48 @@ module JSS
|
|
301
272
|
@need_to_update = true
|
302
273
|
end
|
303
274
|
|
304
|
-
|
305
|
-
|
306
|
-
###
|
307
275
|
### Change the info field in the JSS.
|
308
276
|
###
|
309
277
|
### @param new_val[String]
|
310
278
|
###
|
311
279
|
### @return [void]
|
312
280
|
###
|
313
|
-
def info=
|
281
|
+
def info=(new_val)
|
314
282
|
return nil if new_val == @info
|
315
283
|
### line breaks should be \r
|
316
|
-
new_val = new_val.to_s.
|
284
|
+
new_val = new_val.to_s.tr("\n", "\r")
|
317
285
|
@info = new_val
|
318
286
|
@need_to_update = true
|
319
287
|
end
|
320
288
|
|
321
|
-
|
322
|
-
###
|
323
289
|
### Change the if_in_swupdate field in the JSS
|
324
290
|
###
|
325
291
|
### @param new_val[Boolean]
|
326
292
|
###
|
327
293
|
### @return [void]
|
328
294
|
###
|
329
|
-
def install_if_reported_available=
|
295
|
+
def install_if_reported_available=(new_val)
|
330
296
|
return nil if new_val == @install_if_reported_available
|
331
297
|
new_val = false if new_val.to_s.empty?
|
332
|
-
raise JSS::InvalidDataError,
|
298
|
+
raise JSS::InvalidDataError, 'install_if_reported_available must be boolean true or false' unless JSS::TRUE_FALSE.include? new_val
|
333
299
|
@install_if_reported_available = new_val
|
334
300
|
@need_to_update = true
|
335
301
|
end
|
336
302
|
|
337
|
-
|
338
|
-
|
339
|
-
###
|
340
303
|
### Change the notes field in the JSS.NewLines are converted \r.
|
341
304
|
###
|
342
305
|
### @param new_val[String]
|
343
306
|
###
|
344
307
|
### @return [void]
|
345
308
|
###
|
346
|
-
def notes=
|
309
|
+
def notes=(new_val)
|
347
310
|
return nil if new_val == @notes
|
348
311
|
### line breaks should be \r
|
349
|
-
new_val = new_val.to_s.
|
312
|
+
new_val = new_val.to_s.tr("\n", "\r")
|
350
313
|
@notes = new_val
|
351
314
|
@need_to_update = true
|
352
315
|
end
|
353
316
|
|
354
|
-
###
|
355
317
|
### Change the os_requirements field in the JSS
|
356
318
|
### E.g. 10.5, 10.5.3, 10.6.x
|
357
319
|
###
|
@@ -364,20 +326,20 @@ module JSS
|
|
364
326
|
###
|
365
327
|
### @see JSS.expand_min_os
|
366
328
|
###
|
367
|
-
def os_requirements=
|
329
|
+
def os_requirements=(new_val)
|
368
330
|
### nil should be an empty array
|
369
331
|
new_val = [] if new_val.to_s.empty?
|
370
332
|
|
371
333
|
### if any value starts with >=, expand it
|
372
334
|
case new_val
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
335
|
+
when String
|
336
|
+
new_val = JSS.expand_min_os(new_val) if new_val =~ /^>=/
|
337
|
+
when Array
|
338
|
+
new_val.map! { |a| a =~ /^>=/ ? JSS.expand_min_os(a) : a }
|
339
|
+
new_val.flatten!
|
340
|
+
new_val.uniq!
|
341
|
+
else
|
342
|
+
raise JSS::InvalidDataError, 'os_requirements must be a String or an Array of strings'
|
381
343
|
end
|
382
344
|
### get the array version
|
383
345
|
@os_requirements = JSS.to_s_and_a(new_val)[:arrayform]
|
@@ -393,34 +355,31 @@ module JSS
|
|
393
355
|
### @return [Boolean] can this pkg be installed with the os
|
394
356
|
### given?
|
395
357
|
###
|
396
|
-
def os_ok?
|
358
|
+
def os_ok?(os = nil)
|
397
359
|
JSS.os_ok? @os_requirements, os
|
398
360
|
end
|
399
361
|
|
400
|
-
|
401
|
-
###
|
402
362
|
### Change the priority field in the JSS
|
403
363
|
###
|
404
364
|
### @param new_val[Integer] one of PRIORITIES
|
405
365
|
###
|
406
366
|
### @return [void]
|
407
367
|
###
|
408
|
-
def priority=
|
368
|
+
def priority=(new_val)
|
409
369
|
return nil if new_val == @priority
|
410
370
|
new_val = DEFAULT_PRIORITY if new_val.to_s.empty?
|
411
|
-
raise JSS::InvalidDataError,
|
371
|
+
raise JSS::InvalidDataError, ':priority must be an integer from 1-20' unless PRIORITIES.include? new_val
|
412
372
|
@priority = new_val
|
413
373
|
@need_to_update = true
|
414
374
|
end
|
415
375
|
|
416
|
-
###
|
417
376
|
### Change the reboot-required field in the JSS
|
418
377
|
###
|
419
378
|
### @param new_val[Boolean]
|
420
379
|
###
|
421
380
|
### @return [void]
|
422
381
|
###
|
423
|
-
def reboot_required=
|
382
|
+
def reboot_required=(new_val)
|
424
383
|
return nil if new_val == @reboot_required
|
425
384
|
new_val = false if new_val.to_s.empty?
|
426
385
|
raise JSS::InvalidDataError, "reboot must be boolean 'true' or 'false'" unless JSS::TRUE_FALSE.include? new_val
|
@@ -428,16 +387,13 @@ module JSS
|
|
428
387
|
@need_to_update = true
|
429
388
|
end
|
430
389
|
|
431
|
-
|
432
|
-
|
433
|
-
###
|
434
390
|
### Change the required processor field in the JSS
|
435
391
|
###
|
436
392
|
### @param new_val[String] one of {CPU_TYPES}
|
437
393
|
###
|
438
394
|
### @return [void]
|
439
395
|
###
|
440
|
-
def required_processor=
|
396
|
+
def required_processor=(new_val)
|
441
397
|
return nil if new_val == @required_processor
|
442
398
|
|
443
399
|
new_val = DEFAULT_PROCESSOR if new_val.to_s.empty?
|
@@ -456,45 +412,41 @@ module JSS
|
|
456
412
|
### @return [Boolean] can this pkg be installed with the processor
|
457
413
|
### given?
|
458
414
|
###
|
459
|
-
def processor_ok?
|
415
|
+
def processor_ok?(processor = nil)
|
460
416
|
JSS.processor_ok? @required_processor, processor
|
461
417
|
end
|
462
418
|
|
463
|
-
|
464
|
-
|
465
419
|
### Change the notify field in the JSS
|
466
420
|
###
|
467
421
|
### @param new_val[Boolean]
|
468
422
|
###
|
469
423
|
### @return [void]
|
470
424
|
###
|
471
|
-
def send_notification=
|
425
|
+
def send_notification=(new_val)
|
472
426
|
return nil if new_val == @send_notification
|
473
427
|
new_val = false if new_val.to_s.empty?
|
474
|
-
raise JSS::InvalidDataError,
|
428
|
+
raise JSS::InvalidDataError, 'send_notification must be boolean true or false' unless JSS::TRUE_FALSE.include? new_val
|
475
429
|
@send_notification = new_val
|
476
430
|
@need_to_update = true
|
477
431
|
end
|
478
432
|
|
479
|
-
|
480
433
|
### Change which pkg should be installed if this one can't.
|
481
434
|
###
|
482
435
|
### @param new_val[String] the name of an existing package or "Do Not Install"
|
483
436
|
###
|
484
437
|
### @return [void]
|
485
438
|
###
|
486
|
-
def switch_with_package=
|
439
|
+
def switch_with_package=(new_val)
|
487
440
|
return nil if new_val == @switch_with_package
|
488
441
|
new_val = nil if new_val.to_s.empty?
|
489
442
|
|
490
|
-
raise JSS::NoSuchItemError, "No package named '#{new_val}' exists in the JSS" if new_val
|
443
|
+
raise JSS::NoSuchItemError, "No package named '#{new_val}' exists in the JSS" if new_val && (!self.class.all_names.include? new_val)
|
491
444
|
|
492
445
|
new_val ||= DO_NOT_INSTALL
|
493
446
|
@switch_with_package = new_val
|
494
447
|
@need_to_update = true
|
495
448
|
end
|
496
449
|
|
497
|
-
###
|
498
450
|
### Is this packaged installed on the current machine (via casper)?
|
499
451
|
### We just look for the receipt, which is the @filename less any possible .zip extension.
|
500
452
|
###
|
@@ -504,7 +456,6 @@ module JSS
|
|
504
456
|
@receipt.file?
|
505
457
|
end
|
506
458
|
|
507
|
-
###
|
508
459
|
### Upload a locally-readable file to the master distribution point.
|
509
460
|
### If the file is a directory (like a bundle .pk/.mpkg) it will be zipped before
|
510
461
|
### uploading and the @filename will be adjusted accordingly
|
@@ -521,12 +472,11 @@ module JSS
|
|
521
472
|
###
|
522
473
|
### @return [void]
|
523
474
|
###
|
524
|
-
def upload_master_file
|
525
|
-
|
526
|
-
raise JSS::NoSuchItemError, "Please create this package in the JSS before uploading it." unless @in_jss
|
475
|
+
def upload_master_file(local_file_path, rw_pw, unmount = true)
|
476
|
+
raise JSS::NoSuchItemError, 'Please create this package in the JSS before uploading it.' unless @in_jss
|
527
477
|
|
528
478
|
mdp = JSS::DistributionPoint.master_distribution_point
|
529
|
-
destination = mdp.mount(rw_pw, :rw) +"#{DIST_POINT_PKGS_FOLDER}/#{@filename}"
|
479
|
+
destination = mdp.mount(rw_pw, :rw) + "#{DIST_POINT_PKGS_FOLDER}/#{@filename}"
|
530
480
|
|
531
481
|
local_path = Pathname.new local_file_path
|
532
482
|
raise JSS::NoSuchItemError, "Local file '#{@local_file}' doesn't exist" unless local_path.exist?
|
@@ -534,10 +484,10 @@ module JSS
|
|
534
484
|
### should we zip it?
|
535
485
|
if local_path.directory?
|
536
486
|
begin
|
537
|
-
zipdir = Pathname.new "/tmp/jssgemtmp-#{Time.new.strftime
|
487
|
+
zipdir = Pathname.new "/tmp/jssgemtmp-#{Time.new.strftime '%Y%m%d%H%M%S'}-#{$PROCESS_ID}"
|
538
488
|
zipdir.mkpath
|
539
|
-
zipdir.chmod
|
540
|
-
zipfile = zipdir + (local_path.basename.to_s +
|
489
|
+
zipdir.chmod 0o700
|
490
|
+
zipfile = zipdir + (local_path.basename.to_s + '.zip')
|
541
491
|
|
542
492
|
### go to the same dir as the local file
|
543
493
|
wd = Dir.pwd
|
@@ -546,11 +496,11 @@ module JSS
|
|
546
496
|
### the contents of the zip file have to have the same name as the zip file itself (minus the .zip)
|
547
497
|
### so temporarily rename the source
|
548
498
|
local_path.rename(local_path.parent + @filename)
|
549
|
-
raise
|
499
|
+
raise 'There was a problem zipping the pkg bundle' unless system "/usr/bin/zip -qr '#{zipfile}' '#{@filename}'"
|
550
500
|
|
551
501
|
ensure
|
552
502
|
### rename the source to the original name
|
553
|
-
(local_path.parent + @filename).rename local_path if
|
503
|
+
(local_path.parent + @filename).rename local_path if (local_path.parent + @filename).exist?
|
554
504
|
### go back where we started
|
555
505
|
Dir.chdir wd
|
556
506
|
end # begin
|
@@ -561,13 +511,12 @@ module JSS
|
|
561
511
|
self.filename = zipfile.basename.to_s
|
562
512
|
|
563
513
|
end # if directory
|
564
|
-
|
514
|
+
update
|
565
515
|
FileUtils.copy_entry local_path, destination
|
566
516
|
|
567
517
|
mdp.unmount if unmount
|
568
518
|
end # upload
|
569
519
|
|
570
|
-
|
571
520
|
### Change the name of a package file on the master distribution point.
|
572
521
|
###
|
573
522
|
### @param new_file_name[String]
|
@@ -581,26 +530,23 @@ module JSS
|
|
581
530
|
###
|
582
531
|
### @return [nil]
|
583
532
|
###
|
584
|
-
def update_master_filename(old_file_name, new_file_name, rw_pw
|
533
|
+
def update_master_filename(old_file_name, new_file_name, rw_pw, unmount = true)
|
585
534
|
raise JSS::NoSuchItemError, "#{old_file_name} does not exist in the jss." unless @in_jss
|
586
535
|
mdp = JSS::DistributionPoint.master_distribution_point
|
587
|
-
pkgs_dir =
|
536
|
+
pkgs_dir = mdp.mount(rw_pw, :rw) + DIST_POINT_PKGS_FOLDER.to_s
|
588
537
|
old_file = pkgs_dir + old_file_name
|
538
|
+
raise JSS::NoSuchItemError, "File not found on the master distribution point at #{DIST_POINT_PKGS_FOLDER}/#{old_file_name}." unless \
|
539
|
+
old_file.exist?
|
540
|
+
|
589
541
|
new_file = pkgs_dir + new_file_name
|
590
|
-
if new_file.extname.empty?
|
591
542
|
### use the extension of the original file.
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
old_file.rename new_file
|
596
|
-
else
|
597
|
-
raise JSS::NoSuchItemError, "Original file not found on the master distribution point at #{DIST_POINT_PKGS_FOLDER}/#{old_file_name}."
|
598
|
-
end # if exist
|
543
|
+
new_file = pkgs_dir + (new_file_name + old_file.extname) if new_file.extname.empty?
|
544
|
+
|
545
|
+
old_file.rename new_file
|
599
546
|
mdp.unmount if unmount
|
600
|
-
|
547
|
+
nil
|
601
548
|
end # update_master_filename
|
602
549
|
|
603
|
-
|
604
550
|
### Delete the filename from the master distribution point, if it exists.
|
605
551
|
###
|
606
552
|
### If you'll be uploading several files you can specify unmount as false, and do it manually when all
|
@@ -613,7 +559,7 @@ module JSS
|
|
613
559
|
###
|
614
560
|
### @return [Boolean] was the file deleted?
|
615
561
|
###
|
616
|
-
def delete_master_file
|
562
|
+
def delete_master_file(rw_pw, unmount = true)
|
617
563
|
mdp = JSS::DistributionPoint.master_distribution_point
|
618
564
|
file = mdp.mount(rw_pw, :rw) + "#{DIST_POINT_PKGS_FOLDER}/#{@filename}"
|
619
565
|
if file.exist?
|
@@ -623,10 +569,9 @@ module JSS
|
|
623
569
|
did_it = false
|
624
570
|
end # if exists
|
625
571
|
mdp.unmount if unmount
|
626
|
-
|
572
|
+
did_it
|
627
573
|
end # delete master file
|
628
574
|
|
629
|
-
|
630
575
|
### Delete this package from the JSS, optionally
|
631
576
|
### deleting the master dist point file also.
|
632
577
|
###
|
@@ -637,9 +582,9 @@ module JSS
|
|
637
582
|
###
|
638
583
|
### @param unmount[Boolean] whether or not ot unount the distribution point when finished.
|
639
584
|
###
|
640
|
-
def delete
|
585
|
+
def delete(delete_file: false, rw_pw: nil, unmount: true)
|
641
586
|
super()
|
642
|
-
delete_master_file(rw_pw,
|
587
|
+
delete_master_file(rw_pw, unmount) if delete_file
|
643
588
|
end
|
644
589
|
|
645
590
|
### Install this package via the jamf binary 'install' command from the
|
@@ -682,9 +627,8 @@ module JSS
|
|
682
627
|
###
|
683
628
|
### @todo deal with cert-based https authentication in dist points
|
684
629
|
###
|
685
|
-
def install
|
686
|
-
|
687
|
-
raise JSS::UnsupportedError, "You must have root privileges to install packages" unless JSS.superuser?
|
630
|
+
def install(args = {})
|
631
|
+
raise JSS::UnsupportedError, 'You must have root privileges to install packages' unless JSS.superuser?
|
688
632
|
|
689
633
|
args[:target] ||= '/'
|
690
634
|
|
@@ -698,7 +642,7 @@ module JSS
|
|
698
642
|
# but
|
699
643
|
# in >=9.72: jamf install -package foo.pkg -path http://mycasper.myorg.edu/CasperShare/Packages/foo.pkg
|
700
644
|
#
|
701
|
-
append_at_vers = JSS.parse_jss_version(
|
645
|
+
append_at_vers = JSS.parse_jss_version('9.72')[:version]
|
702
646
|
our_vers = JSS.parse_jss_version(JSS::API.server.raw_version)[:version]
|
703
647
|
no_filename_in_url = (our_vers < append_at_vers)
|
704
648
|
|
@@ -713,15 +657,15 @@ module JSS
|
|
713
657
|
mdp = JSS::DistributionPoint.my_distribution_point
|
714
658
|
|
715
659
|
### how do we access our dist. point? with http?
|
716
|
-
if mdp.http_downloads_enabled
|
660
|
+
if mdp.http_downloads_enabled && !(args[:no_http])
|
717
661
|
using_http = true
|
718
662
|
src_path = mdp.http_url
|
719
663
|
if mdp.username_password_required
|
720
|
-
raise JSS::MissingDataError,
|
721
|
-
raise JSS::InvaldDatatError,
|
664
|
+
raise JSS::MissingDataError, 'No password provided for http download' unless ro_pw
|
665
|
+
raise JSS::InvaldDatatError, 'Incorrect password for http access to distribution point.' unless mdp.check_pw(:http, ro_pw)
|
722
666
|
# insert the name and pw into the uri
|
723
667
|
# reserved_chars = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}]") # we'll escape all the chars that aren't unreserved
|
724
|
-
src_path = src_path.sub(%r{(https?://)(\S)}, "#{
|
668
|
+
src_path = src_path.sub(%r{(https?://)(\S)}, "#{Regexp.last_match(1)}#{CGI.escape mdp.http_username}:#{CGI.escape ro_pw}@#{Regexp.last_match(2)}")
|
725
669
|
end
|
726
670
|
|
727
671
|
# or with filesharing?
|
@@ -735,12 +679,12 @@ module JSS
|
|
735
679
|
end # if args[:alt_download_url]
|
736
680
|
|
737
681
|
if using_http
|
738
|
-
src_path +=
|
682
|
+
src_path += @filename.to_s unless no_filename_in_url
|
739
683
|
end
|
740
684
|
|
741
685
|
### are we doing "fill existing users" or "fill user template"?
|
742
|
-
do_feu = args[:feu] ?
|
743
|
-
do_fut = args[:fut] ?
|
686
|
+
do_feu = args[:feu] ? '-feu' : ''
|
687
|
+
do_fut = args[:fut] ? '-fut' : ''
|
744
688
|
|
745
689
|
### the install args for jamf
|
746
690
|
command_args = "-package '#{@filename}' -path '#{src_path}' -target '#{args[:target]}' #{do_feu} #{do_fut} -showProgress -verbose"
|
@@ -749,21 +693,17 @@ module JSS
|
|
749
693
|
install_out = JSS::Client.run_jamf :install, command_args, args[:verbose]
|
750
694
|
|
751
695
|
install_out =~ %r{<exitCode>(\d+)</exitCode>}
|
752
|
-
install_exit =
|
753
|
-
install_exit ||=
|
754
|
-
|
696
|
+
install_exit = Regexp.last_match(1) ? Regexp.last_match(1).to_i : nil
|
697
|
+
install_exit ||= $CHILD_STATUS.exitstatus
|
755
698
|
|
756
|
-
if
|
699
|
+
if args.include? :unmount
|
757
700
|
mdp.unmount unless using_http
|
758
701
|
end
|
759
702
|
|
760
|
-
|
703
|
+
install_exit.zero? ? true : false
|
761
704
|
end
|
762
705
|
|
763
|
-
###
|
764
|
-
### @note This code must be run as root to uninstall packages
|
765
|
-
###
|
766
|
-
### Causes the pkg to be uninstalled via the jamf command.
|
706
|
+
### Uninstall this pkg via the jamf command.
|
767
707
|
###
|
768
708
|
### @param args[Hash] the arguments for installation
|
769
709
|
###
|
@@ -777,28 +717,29 @@ module JSS
|
|
777
717
|
###
|
778
718
|
### @return [Process::Status] the result of the 'jamf uninstall' command
|
779
719
|
###
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
720
|
+
### @note This code must be run as root to uninstall packages
|
721
|
+
###
|
722
|
+
def uninstall(args = {})
|
723
|
+
unless removable?
|
724
|
+
raise JSS::UnsupportedError, \
|
725
|
+
'This package cannot be uninstalled. Please use CasperAdmin to index it and allow uninstalls'
|
726
|
+
end
|
727
|
+
raise JSS::UnsupportedError, 'You must have root privileges to uninstall packages' unless JSS.superuser?
|
785
728
|
args[:target] ||= '/'
|
786
729
|
|
787
730
|
### are we doing "fill existing users" or "fill user template"?
|
788
|
-
do_feu = args[:feu] ?
|
789
|
-
do_fut = args[:fut] ?
|
731
|
+
do_feu = args[:feu] ? '-feu' : ''
|
732
|
+
do_fut = args[:fut] ? '-fut' : ''
|
790
733
|
|
791
734
|
### use jamf binary to uninstall the pkg
|
792
735
|
jamf_opts = "-target '#{args[:target]}' -id '#{@id}' #{do_feu} #{do_fut}"
|
793
736
|
|
794
737
|
### run it via a client
|
795
|
-
|
738
|
+
JSS::Client.run_jamf 'uninstall', jamf_opts, args[:verbose]
|
796
739
|
|
797
|
-
|
740
|
+
$CHILD_STATUS
|
798
741
|
end
|
799
742
|
|
800
|
-
|
801
|
-
|
802
743
|
### What type of package is this?
|
803
744
|
###
|
804
745
|
### @return [Symbol] :pkg or :dmg or:unknown
|
@@ -811,42 +752,9 @@ module JSS
|
|
811
752
|
end
|
812
753
|
end
|
813
754
|
|
814
|
-
|
815
|
-
################################
|
816
|
-
### Private Instance Methods
|
755
|
+
### Aliases
|
817
756
|
################################
|
818
757
|
|
819
|
-
private
|
820
|
-
|
821
|
-
|
822
|
-
###
|
823
|
-
### Return the REST XML for this pkg, with the current values,
|
824
|
-
### for saving or updating
|
825
|
-
###
|
826
|
-
def rest_xml
|
827
|
-
doc = REXML::Document.new APIConnection::XML_HEADER
|
828
|
-
pkg = doc.add_element "package"
|
829
|
-
pkg.add_element('allow_uninstalled').text = @allow_uninstalled
|
830
|
-
pkg.add_element('boot_volume_required').text = @boot_volume_required
|
831
|
-
pkg.add_element('category').text = @category.to_s.casecmp("No category assigned") == 0 ? "" : @category
|
832
|
-
pkg.add_element('filename').text = @filename
|
833
|
-
pkg.add_element('fill_existing_users').text = @fill_existing_users
|
834
|
-
pkg.add_element('fill_user_template').text = @fill_user_template
|
835
|
-
pkg.add_element('info').text = @info
|
836
|
-
pkg.add_element('install_if_reported_available').text = @install_if_reported_available
|
837
|
-
pkg.add_element('name').text = @name
|
838
|
-
pkg.add_element('notes').text = @notes
|
839
|
-
pkg.add_element('os_requirements').text = JSS.to_s_and_a(@os_requirements)[:stringform]
|
840
|
-
pkg.add_element('priority').text = @priority
|
841
|
-
pkg.add_element('reboot_required').text = @reboot_required
|
842
|
-
pkg.add_element('required_processor').text = @required_processor.to_s.empty? ? "None" : @required_processor
|
843
|
-
pkg.add_element('send_notification').text = @send_notification
|
844
|
-
pkg.add_element('switch_with_package').text = @switch_with_package
|
845
|
-
return doc.to_s
|
846
|
-
end # rest xml
|
847
|
-
|
848
|
-
public
|
849
|
-
|
850
758
|
# aliases under their methods seem to confuse the YARD documenter, so I'm putting them all here.
|
851
759
|
alias feu fill_existing_users
|
852
760
|
alias feu? fill_existing_users
|
@@ -874,9 +782,35 @@ module JSS
|
|
874
782
|
alias cpu_type= required_processor=
|
875
783
|
alias notify= send_notification=
|
876
784
|
|
785
|
+
### Private Instance Methods
|
786
|
+
################################
|
877
787
|
|
788
|
+
private
|
878
789
|
|
879
|
-
|
790
|
+
### Return the REST XML for this pkg, with the current values,
|
791
|
+
### for saving or updating
|
792
|
+
###
|
793
|
+
def rest_xml
|
794
|
+
doc = REXML::Document.new APIConnection::XML_HEADER
|
795
|
+
pkg = doc.add_element 'package'
|
796
|
+
pkg.add_element('allow_uninstalled').text = @allow_uninstalled
|
797
|
+
pkg.add_element('boot_volume_required').text = @boot_volume_required
|
798
|
+
pkg.add_element('category').text = @category.to_s.casecmp('No category assigned').zero? ? '' : @category
|
799
|
+
pkg.add_element('filename').text = @filename
|
800
|
+
pkg.add_element('fill_existing_users').text = @fill_existing_users
|
801
|
+
pkg.add_element('fill_user_template').text = @fill_user_template
|
802
|
+
pkg.add_element('info').text = @info
|
803
|
+
pkg.add_element('install_if_reported_available').text = @install_if_reported_available
|
804
|
+
pkg.add_element('name').text = @name
|
805
|
+
pkg.add_element('notes').text = @notes
|
806
|
+
pkg.add_element('os_requirements').text = JSS.to_s_and_a(@os_requirements)[:stringform]
|
807
|
+
pkg.add_element('priority').text = @priority
|
808
|
+
pkg.add_element('reboot_required').text = @reboot_required
|
809
|
+
pkg.add_element('required_processor').text = @required_processor.to_s.empty? ? 'None' : @required_processor
|
810
|
+
pkg.add_element('send_notification').text = @send_notification
|
811
|
+
pkg.add_element('switch_with_package').text = @switch_with_package
|
812
|
+
doc.to_s
|
813
|
+
end # rest xml
|
880
814
|
|
881
815
|
end # class Package
|
882
816
|
|