ruby-jss 0.6.4 → 0.6.5
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/lib/jss.rb +3 -0
- data/lib/jss/api_object.rb +27 -23
- data/lib/jss/api_object/account.rb +160 -0
- data/lib/jss/api_object/computer.rb +30 -12
- data/lib/jss/api_object/computer_invitation.rb +206 -0
- data/lib/jss/api_object/creatable.rb +7 -6
- data/lib/jss/api_object/distribution_point.rb +11 -6
- data/lib/jss/api_object/group.rb +13 -13
- data/lib/jss/api_object/osx_configuration_profile.rb +61 -59
- data/lib/jss/api_object/package.rb +41 -12
- data/lib/jss/api_object/restricted_software.rb +86 -0
- data/lib/jss/api_object/script.rb +1 -1
- data/lib/jss/api_object/self_servable.rb +55 -58
- data/lib/jss/api_object/updatable.rb +1 -1
- data/lib/jss/client.rb +1 -1
- data/lib/jss/version.rb +1 -1
- metadata +5 -2
@@ -86,11 +86,6 @@ module JSS
|
|
86
86
|
### The possible values for cpu_type (required_processor) in a JSS package
|
87
87
|
CPU_TYPES = ["None", "x86", "ppc"]
|
88
88
|
|
89
|
-
# TO DO - this is redundant with DEFAULT_PROCESSOR, but both are in use
|
90
|
-
# clean them up!
|
91
|
-
### which is default? there must be one to make a new pkg
|
92
|
-
DEFAULT_CPU_TYPE = "None"
|
93
|
-
|
94
89
|
### the possible priorities
|
95
90
|
PRIORITIES = (1..20)
|
96
91
|
|
@@ -190,7 +185,7 @@ module JSS
|
|
190
185
|
|
191
186
|
@priority = @init_data[:priority] || DEFAULT_PRIORITY
|
192
187
|
@reboot_required = @init_data[:reboot_required]
|
193
|
-
@required_processor = @init_data[:required_processor] ||
|
188
|
+
@required_processor = @init_data[:required_processor] || DEFAULT_PROCESSOR
|
194
189
|
@required_processor = nil if @required_processor.to_s.casecmp('none') == 0
|
195
190
|
@send_notification = @init_data[:send_notification]
|
196
191
|
@switch_with_package = @init_data[:switch_with_package] || DO_NOT_INSTALL
|
@@ -270,7 +265,7 @@ module JSS
|
|
270
265
|
new_val = nil if new_val == ''
|
271
266
|
new_val ||= @name
|
272
267
|
return nil if new_val == @filename
|
273
|
-
$stderr.puts "WARNING: you must
|
268
|
+
$stderr.puts "WARNING: you must change the filename on the master Distribution Point. See JSS::Package.update_master_filename." if @in_jss
|
274
269
|
@filename = new_val
|
275
270
|
@need_to_update = true
|
276
271
|
end
|
@@ -573,6 +568,39 @@ module JSS
|
|
573
568
|
end # upload
|
574
569
|
|
575
570
|
|
571
|
+
### Change the name of a package file on the master distribution point.
|
572
|
+
###
|
573
|
+
### @param new_file_name[String]
|
574
|
+
###
|
575
|
+
### @param old_file_name[default: @filename, String]
|
576
|
+
###
|
577
|
+
### @param unmount[Boolean] whether or not ot unount the distribution point when finished.
|
578
|
+
###
|
579
|
+
### @param rw_pw[String,Symbol] the password for the read/write account on the master Distribution Point,
|
580
|
+
### or :prompt, or :stdin# where # is the line of stdin containing the password See {JSS::DistributionPoint#mount}
|
581
|
+
###
|
582
|
+
### @return [nil]
|
583
|
+
###
|
584
|
+
def update_master_filename(old_file_name, new_file_name, rw_pw , unmount = true )
|
585
|
+
raise JSS::NoSuchItemError, "#{old_file_name} does not exist in the jss." unless @in_jss
|
586
|
+
mdp = JSS::DistributionPoint.master_distribution_point
|
587
|
+
pkgs_dir = mdp.mount(rw_pw, :rw) + "#{DIST_POINT_PKGS_FOLDER}"
|
588
|
+
old_file = pkgs_dir + old_file_name
|
589
|
+
new_file = pkgs_dir + new_file_name
|
590
|
+
if new_file.extname.empty?
|
591
|
+
### use the extension of the original file.
|
592
|
+
new_file = pkgs_dir + (new_file_name + old_file.extname)
|
593
|
+
end
|
594
|
+
if old_file.exist?
|
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
|
599
|
+
mdp.unmount if unmount
|
600
|
+
return nil
|
601
|
+
end # update_master_filename
|
602
|
+
|
603
|
+
|
576
604
|
### Delete the filename from the master distribution point, if it exists.
|
577
605
|
###
|
578
606
|
### If you'll be uploading several files you can specify unmount as false, and do it manually when all
|
@@ -692,8 +720,8 @@ module JSS
|
|
692
720
|
raise JSS::MissingDataError, "No password provided for http download" unless ro_pw
|
693
721
|
raise JSS::InvaldDatatError, "Incorrect password for http access to distribution point." unless mdp.check_pw(:http, ro_pw)
|
694
722
|
# insert the name and pw into the uri
|
695
|
-
reserved_chars = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}]") # we'll escape all the chars that aren't unreserved
|
696
|
-
src_path = src_path.sub(%r{(https?://)(\S)}, "#{$1}#{
|
723
|
+
# 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)}, "#{$1}#{CGI.escape mdp.http_username}:#{CGI.escape ro_pw}@#{$2}")
|
697
725
|
end
|
698
726
|
|
699
727
|
# or with filesharing?
|
@@ -703,11 +731,12 @@ module JSS
|
|
703
731
|
end
|
704
732
|
|
705
733
|
# look at the pkgs folder
|
706
|
-
src_path += "#{DIST_POINT_PKGS_FOLDER}"
|
734
|
+
src_path += "#{DIST_POINT_PKGS_FOLDER}/"
|
707
735
|
end # if args[:alt_download_url]
|
708
736
|
|
709
|
-
|
710
|
-
|
737
|
+
if using_http
|
738
|
+
src_path += "#{@filename}" unless no_filename_in_url
|
739
|
+
end
|
711
740
|
|
712
741
|
### are we doing "fill existing users" or "fill user template"?
|
713
742
|
do_feu = args[:feu] ? "-feu" : ""
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module JSS
|
2
|
+
#####################################
|
3
|
+
### Classes
|
4
|
+
#####################################
|
5
|
+
|
6
|
+
###
|
7
|
+
### Restricted Software in the JSS.
|
8
|
+
###
|
9
|
+
### This class only supports showing of object data.
|
10
|
+
###
|
11
|
+
### @see JSS::APIObject
|
12
|
+
###
|
13
|
+
class RestrictedSoftware < JSS::APIObject
|
14
|
+
|
15
|
+
#####################################
|
16
|
+
### Mix-Ins
|
17
|
+
#####################################
|
18
|
+
|
19
|
+
include JSS::Scopable
|
20
|
+
|
21
|
+
#####################################
|
22
|
+
### Class Constants
|
23
|
+
#####################################
|
24
|
+
|
25
|
+
### The base for REST resources of this class
|
26
|
+
RSRC_BASE = "restrictedsoftware"
|
27
|
+
|
28
|
+
### the hash key used for the JSON list output of all objects in the JSS
|
29
|
+
RSRC_LIST_KEY = :restricted_software
|
30
|
+
|
31
|
+
### The hash key used for the JSON object output.
|
32
|
+
### It's also used in various error messages
|
33
|
+
RSRC_OBJECT_KEY = :restricted_software
|
34
|
+
|
35
|
+
### these keys, as well as :id and :name, are present in valid API JSON data for this class
|
36
|
+
VALID_DATA_KEYS = [:scope]
|
37
|
+
|
38
|
+
### Our scopes deal with computers
|
39
|
+
SCOPE_TARGET_KEY = :computers
|
40
|
+
|
41
|
+
#####################################
|
42
|
+
### Attributes
|
43
|
+
#####################################
|
44
|
+
|
45
|
+
### The values returned in the General, Location, and Purchasing subsets are stored as direct attributes
|
46
|
+
### Location and Purchasing are defined in the Locatable and Purchasable mixin modules.
|
47
|
+
### Here's General, in alphabetical order
|
48
|
+
|
49
|
+
### @return [String] the process name
|
50
|
+
attr_reader :process_name
|
51
|
+
|
52
|
+
### @return [Boolean] whether to return match exact process name
|
53
|
+
attr_reader :match_exact_process_name
|
54
|
+
|
55
|
+
### @return [Boolean] whether to send a notification
|
56
|
+
attr_reader :send_notification
|
57
|
+
|
58
|
+
### @return [Boolean] whether to kill the running process
|
59
|
+
attr_reader :kill_process
|
60
|
+
|
61
|
+
### @return [Boolean] whether to delete the executable
|
62
|
+
attr_reader :delete_executable
|
63
|
+
|
64
|
+
### @return [String] message displayed to the user
|
65
|
+
attr_reader :display_message
|
66
|
+
|
67
|
+
### @return [Hash] the :name and :id of the site for this machine
|
68
|
+
attr_reader :site
|
69
|
+
|
70
|
+
#####################################
|
71
|
+
### Instance Methods
|
72
|
+
#####################################
|
73
|
+
|
74
|
+
def initialize(args = {})
|
75
|
+
super args, []
|
76
|
+
|
77
|
+
@process_name = @init_data[:general][:process_name]
|
78
|
+
@match_exact_process_name = @init_data[:general][:match_exact_process_name]
|
79
|
+
@send_notification = @init_data[:general][:send_notification]
|
80
|
+
@kill_process = @init_data[:general][:kill_process]
|
81
|
+
@delete_executable = @init_data[:general][:delete_executable]
|
82
|
+
@display_message = @init_data[:general][:display_message]
|
83
|
+
@site = JSS::APIObject.get_name(@init_data[:general][:site])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -193,7 +193,7 @@ module JSS
|
|
193
193
|
@name = new_val
|
194
194
|
|
195
195
|
### if our REST resource is based on the name, update that too
|
196
|
-
@rest_rsrc = "#{RSRC_BASE}/name/#{
|
196
|
+
@rest_rsrc = "#{RSRC_BASE}/name/#{CGI.escape @name}" if @rest_rsrc.include? '/name/'
|
197
197
|
@need_to_update = true
|
198
198
|
end #name=
|
199
199
|
|
@@ -1,25 +1,25 @@
|
|
1
1
|
### Copyright 2016 Pixar
|
2
|
-
###
|
2
|
+
###
|
3
3
|
### Licensed under the Apache License, Version 2.0 (the "Apache License")
|
4
4
|
### with the following modification; you may not use this file except in
|
5
5
|
### compliance with the Apache License and the following modification to it:
|
6
6
|
### Section 6. Trademarks. is deleted and replaced with:
|
7
|
-
###
|
7
|
+
###
|
8
8
|
### 6. Trademarks. This License does not grant permission to use the trade
|
9
9
|
### names, trademarks, service marks, or product names of the Licensor
|
10
10
|
### and its affiliates, except as required to comply with Section 4(c) of
|
11
11
|
### the License and to reproduce the content of the NOTICE file.
|
12
|
-
###
|
12
|
+
###
|
13
13
|
### You may obtain a copy of the Apache License at
|
14
|
-
###
|
14
|
+
###
|
15
15
|
### http://www.apache.org/licenses/LICENSE-2.0
|
16
|
-
###
|
16
|
+
###
|
17
17
|
### Unless required by applicable law or agreed to in writing, software
|
18
18
|
### distributed under the Apache License with the above modification is
|
19
19
|
### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
20
20
|
### KIND, either express or implied. See the Apache License for the specific
|
21
21
|
### language governing permissions and limitations under the Apache License.
|
22
|
-
###
|
22
|
+
###
|
23
23
|
###
|
24
24
|
|
25
25
|
###
|
@@ -41,19 +41,19 @@ module JSS
|
|
41
41
|
###
|
42
42
|
### The JSS objects that have Self Service data return it in a :self_service subset,
|
43
43
|
### which all have similar data, a hash with at least these keys:
|
44
|
-
### - :self_service_description
|
45
|
-
### - :self_service_icon
|
44
|
+
### - :self_service_description
|
45
|
+
### - :self_service_icon
|
46
46
|
###
|
47
47
|
### Most also have:
|
48
48
|
### - :feature_on_main_page
|
49
49
|
### - :self_service_categories
|
50
50
|
###
|
51
|
-
### iOS Profiles in self service have this key:
|
52
|
-
### - :security
|
51
|
+
### iOS Profiles in self service have this key:
|
52
|
+
### - :security
|
53
53
|
###
|
54
54
|
### Additionally, items that apper in OS X SlfSvc have these keys:
|
55
55
|
### - :install_button_text
|
56
|
-
### - :force_users_to_view_description
|
56
|
+
### - :force_users_to_view_description
|
57
57
|
###
|
58
58
|
### See the attribute definitions for details of these values and structures.
|
59
59
|
###
|
@@ -71,14 +71,14 @@ module JSS
|
|
71
71
|
### - Define the constant SELF_SERVICE_PAYLOAD which contains one of :policy, :profile, or :app
|
72
72
|
### - Call {#parse_self_service} in the subclass's constructor after calling super
|
73
73
|
### - Include the result of {#self_service_xml} in their #rest_xml output
|
74
|
-
### - Define the method #in_self_service? which returns a Boolean indicating that the item is
|
74
|
+
### - Define the method #in_self_service? which returns a Boolean indicating that the item is
|
75
75
|
### available in self service. Different API objects indicate this in different ways.
|
76
76
|
### - Define the method #user_removable? which returns Boolean indicating that the item (a profile)
|
77
77
|
### can be removed by the user in SSvc. OS X profiles store this in the :user_removable key of the
|
78
78
|
### :general subset as a boolean, whereas iOS profiles stor it in :security as one of 3 strings
|
79
79
|
###
|
80
80
|
###
|
81
|
-
### Notes:
|
81
|
+
### Notes:
|
82
82
|
### - Self service icons cannot be modified via this code. Use the Web UI.
|
83
83
|
### - There an API bug in handling categories, and all but the last one are ommitted. Until this is fixed, categories
|
84
84
|
### cannot be saved via this code since that would cause data-loss when more than one category is applied.
|
@@ -90,19 +90,19 @@ module JSS
|
|
90
90
|
#####################################
|
91
91
|
|
92
92
|
SELF_SERVABLE = true
|
93
|
-
|
93
|
+
|
94
94
|
IOS_PROFILE_REMOVAL_OPTIONS = ["Always", "With Authorization", "Never"]
|
95
|
-
|
95
|
+
|
96
96
|
#####################################
|
97
97
|
### Variables
|
98
98
|
#####################################
|
99
|
-
|
100
|
-
|
99
|
+
|
100
|
+
|
101
101
|
#####################################
|
102
102
|
### Attribtues
|
103
103
|
#####################################
|
104
104
|
|
105
|
-
|
105
|
+
|
106
106
|
### @return [String] The verbage that appears in SelfSvc for this item
|
107
107
|
attr_reader :self_service_description
|
108
108
|
|
@@ -127,7 +127,7 @@ module JSS
|
|
127
127
|
### - :display_in => [Boolean] should the item be displayed in this category in SSvc? (OSX SSvc only)
|
128
128
|
### - :feature_in => [Boolean] should the item be featured in this category in SSVC? (OSX SSvc only)
|
129
129
|
###
|
130
|
-
### NOTE: as of Casper 9.61 there's a bug in the JSON output from the API, and only the last
|
130
|
+
### NOTE: as of Casper 9.61 there's a bug in the JSON output from the API, and only the last
|
131
131
|
### category is returned, if more than one are set.
|
132
132
|
###
|
133
133
|
attr_reader :self_service_categories
|
@@ -145,7 +145,7 @@ module JSS
|
|
145
145
|
|
146
146
|
### @return [String] The text label on the install button in SSvc (OSX SSvc only)
|
147
147
|
attr_reader :self_service_install_button_text
|
148
|
-
|
148
|
+
|
149
149
|
### @return [Boolean] Should an extra window appear before the user can install the item? (OSX SSvc only)
|
150
150
|
attr_reader :self_service_force_users_to_view_description
|
151
151
|
|
@@ -165,32 +165,29 @@ module JSS
|
|
165
165
|
def parse_self_service
|
166
166
|
@init_data[:self_service] ||= {}
|
167
167
|
ss_data = @init_data[:self_service]
|
168
|
-
|
168
|
+
|
169
169
|
@self_service_description = ss_data[:self_service_description]
|
170
170
|
@self_service_icon = ss_data[:self_service_icon]
|
171
|
-
|
171
|
+
|
172
172
|
@self_service_feature_on_main_page = ss_data[:feature_on_main_page]
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
ss_data[:self_service_categories][:category]
|
177
|
-
]
|
178
|
-
|
173
|
+
|
174
|
+
@self_service_categories = ss_data[:self_service_categories]
|
175
|
+
|
179
176
|
# make this an empty hash if needed
|
180
177
|
@self_service_security = ss_data[:security] || {}
|
181
|
-
|
182
|
-
# if this is an osx profile, set @self_service_security[:removal_disallowed] to "Always" or "Never"
|
178
|
+
|
179
|
+
# if this is an osx profile, set @self_service_security[:removal_disallowed] to "Always" or "Never"
|
183
180
|
# to indicate the boolean :user_removable
|
184
181
|
if @init_data[:general].keys.include? :user_removable
|
185
182
|
@self_service_security[:removal_disallowed] = @init_data[:general][:user_removable] ? "Always" : "Never"
|
186
183
|
end
|
187
|
-
|
184
|
+
|
188
185
|
@self_service_install_button_text = ss_data[:install_button_text]
|
189
186
|
@self_service_force_users_to_view_description = ss_data[:force_users_to_view_description]
|
190
187
|
|
191
188
|
end
|
192
189
|
|
193
|
-
|
190
|
+
|
194
191
|
###
|
195
192
|
###
|
196
193
|
### Setters
|
@@ -218,9 +215,9 @@ module JSS
|
|
218
215
|
@self_service_install_button_text = new_val.strip
|
219
216
|
@need_to_update = true
|
220
217
|
end
|
221
|
-
|
218
|
+
|
222
219
|
###
|
223
|
-
### @param new_val[Boolean] should this appear on the main SelfSvc page?
|
220
|
+
### @param new_val[Boolean] should this appear on the main SelfSvc page?
|
224
221
|
###
|
225
222
|
### @return [void]
|
226
223
|
###
|
@@ -230,9 +227,9 @@ module JSS
|
|
230
227
|
@self_service_feature_on_main_page = new_val
|
231
228
|
@need_to_update = true
|
232
229
|
end
|
233
|
-
|
230
|
+
|
234
231
|
###
|
235
|
-
### @param new_val[Boolean] should this appear on the main SelfSvc page?
|
232
|
+
### @param new_val[Boolean] should this appear on the main SelfSvc page?
|
236
233
|
###
|
237
234
|
### @return [void]
|
238
235
|
###
|
@@ -243,7 +240,7 @@ module JSS
|
|
243
240
|
@self_service_force_users_to_view_description = new_val
|
244
241
|
@need_to_update = true
|
245
242
|
end
|
246
|
-
|
243
|
+
|
247
244
|
###
|
248
245
|
### Add or change one of the categories for this item in SSvc.
|
249
246
|
###
|
@@ -260,21 +257,21 @@ module JSS
|
|
260
257
|
raise JSS::NoSuchItemError, "No category '#{new_cat}' in the JSS" unless JSS::Category.all_names(:refresh).include? new_cat
|
261
258
|
raise JSS::InvalidDataError, "display_in must be true or false" unless JSS::TRUE_FALSE.include? display_in
|
262
259
|
raise JSS::InvalidDataError, "feature_in must be true or false" unless JSS::TRUE_FALSE.include? feature_in
|
263
|
-
|
260
|
+
|
264
261
|
new_data = {:name => new_cat, :display_in => display_in, :feature_in => feature_in }
|
265
|
-
|
262
|
+
|
266
263
|
# see if this category is already among our categories.
|
267
264
|
idx = @self_service_categories.index{|c| c[new_cat]}
|
268
|
-
|
269
|
-
if idx
|
265
|
+
|
266
|
+
if idx
|
270
267
|
@self_service_categories[idx] = new_data
|
271
268
|
else
|
272
269
|
@self_service_categories << new_data
|
273
270
|
end
|
274
|
-
|
271
|
+
|
275
272
|
@need_to_update = true
|
276
273
|
end
|
277
|
-
|
274
|
+
|
278
275
|
###
|
279
276
|
### Remove a category from those for this item in SSvc
|
280
277
|
###
|
@@ -296,17 +293,17 @@ module JSS
|
|
296
293
|
### @return [void]
|
297
294
|
###
|
298
295
|
def profile_can_be_removed (new_val)
|
299
|
-
|
300
|
-
new_val = "Always" if new_val === true
|
296
|
+
|
297
|
+
new_val = "Always" if new_val === true
|
301
298
|
new_val = "Never" if new_val === false
|
302
|
-
|
299
|
+
|
303
300
|
return nil if new_val == @self_service_security[:removal_disallowed]
|
304
301
|
raise JSS::InvalidDataError, "" unless IOS_PROFILE_REMOVAL_OPTIONS.include? new_val
|
305
|
-
|
302
|
+
|
306
303
|
@self_service_security[:removal_disallowed] = new_val
|
307
304
|
end
|
308
|
-
|
309
|
-
|
305
|
+
|
306
|
+
|
310
307
|
###
|
311
308
|
### @api private
|
312
309
|
###
|
@@ -317,16 +314,16 @@ module JSS
|
|
317
314
|
### @return [REXML::Element]
|
318
315
|
###
|
319
316
|
def self_service_xml
|
320
|
-
|
317
|
+
|
321
318
|
ssvc = REXML::Element.new('self_service')
|
322
|
-
|
319
|
+
|
323
320
|
return ssvc unless self.in_self_service?
|
324
|
-
|
321
|
+
|
325
322
|
ssvc.add_element('self_service_description').text = @self_service_description
|
326
323
|
ssvc.add_element('feature_on_main_page').text = @self_service_feature_on_main_page
|
327
|
-
|
324
|
+
|
328
325
|
### TEMPORARY - re-enable this when the category bug is fixed.
|
329
|
-
|
326
|
+
|
330
327
|
# cats = ssvc.add_element('self_service_categories')
|
331
328
|
# @self_service_categories.each do |cat|
|
332
329
|
# catelem = cats.add_element('category')
|
@@ -334,22 +331,22 @@ module JSS
|
|
334
331
|
# catelem.add_element('display_in').text = cat[:display_in] if cat.keys.include? :display_in
|
335
332
|
# catelem.add_element('feature_in').text = cat[:feature_in] if cat.keys.include? :feature_in
|
336
333
|
# end
|
337
|
-
|
334
|
+
|
338
335
|
unless @self_service_security.empty?
|
339
336
|
sec = ssvc.add_element('security')
|
340
337
|
sec.add_element('removal_disallowed').text = @self_service_security[:removal_disallowed] if @self_service_security[:removal_disallowed]
|
341
338
|
sec.add_element('password').text = @self_service_security[:password] if @self_service_security[:password]
|
342
339
|
end
|
343
|
-
|
340
|
+
|
344
341
|
ssvc.add_element('install_button_text').text = @self_service_install_button_text if @self_service_install_button_text
|
345
342
|
ssvc.add_element('force_users_to_view_description').text = @self_service_force_users_to_view_description unless @self_service_force_users_to_view_description.nil?
|
346
343
|
|
347
344
|
return ssvc
|
348
345
|
end
|
349
|
-
|
346
|
+
|
350
347
|
### aliases
|
351
348
|
alias change_self_service_category add_self_service_category
|
352
|
-
|
349
|
+
|
353
350
|
end # module SelfServable
|
354
351
|
|
355
352
|
end # module JSS
|