xolo-admin 1.0.0 → 2.0.2

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.
@@ -126,6 +126,13 @@ module Xolo
126
126
  # skip things not given on the command line
127
127
  next unless cli_cmd_opts["#{key}_given"]
128
128
 
129
+ if deets[:add_only] && edit_command?
130
+ raise ArgumentError, "Option '--#{key.to_s.gsub '_', '-'}' is only valid for 'add-' commands."
131
+
132
+ elsif deets[:edit_only] && add_command?
133
+ raise ArgumentError, "Option '--#{key.to_s.gsub '_', '-'}' is only valid for 'edit-' commands."
134
+ end
135
+
129
136
  # skip things that shouldn't be validated
130
137
  next unless deets[:validate]
131
138
 
@@ -196,7 +203,7 @@ module Xolo
196
203
  if cli_cmd.command == Xolo::Admin::Options::ADD_TITLE_CMD
197
204
  err =
198
205
  if title_exists
199
- 'already exists in Xolo'
206
+ "title '#{val}' already exists in Xolo"
200
207
  elsif val !~ /\A[a-z0-9-][a-z0-9-]+\z/
201
208
  TITLE_ATTRS[:title][:invalid_msg]
202
209
  else
@@ -207,7 +214,7 @@ module Xolo
207
214
  elsif Xolo::Admin::Options::MUST_EXIST_COMMANDS.include?(cli_cmd.command)
208
215
  return val if title_exists
209
216
 
210
- err = "doesn't exist in Xolo"
217
+ err = "title '#{val}' doesn't exist in Xolo"
211
218
 
212
219
  # any other command
213
220
  else
@@ -217,10 +224,22 @@ module Xolo
217
224
  raise_invalid_data_error val, err
218
225
  end
219
226
 
220
- # validate a title display-name. Must be 3+ chars long
227
+ # validate the title type
221
228
  #
222
229
  # @param val [Object] The value to validate
223
230
  #
231
+ # @return [String] The valid value
232
+ ########################
233
+ def validate_type(val)
234
+ val = val.to_s.strip.downcase.to_sym
235
+ return val if Xolo::TITLE_TYPES.include? val
236
+
237
+ raise_invalid_data_error val, TITLE_ATTRS[:type][:invalid_msg]
238
+ end
239
+
240
+ # validate a title display-name. Must be 3+ chars long
241
+ #
242
+ # @param val [Object] The value to validate
224
243
  #
225
244
  # @return [String] The valid value
226
245
  ##########################
@@ -267,7 +286,7 @@ module Xolo
267
286
  val = val.to_s.strip
268
287
  return val if val.end_with? Xolo::DOTAPP
269
288
 
270
- raise_invalid_data_error val, TITLE_ATTRS[:app_bundle_id][:invalid_msg]
289
+ raise_invalid_data_error val, TITLE_ATTRS[:app_name][:invalid_msg]
271
290
  end
272
291
 
273
292
  # validate a title app_bundle_id. Must include at least one dot
@@ -296,6 +315,32 @@ module Xolo
296
315
  raise_invalid_data_error val, TITLE_ATTRS[:version_script][:invalid_msg]
297
316
  end
298
317
 
318
+ # validate a patch source name. Must exist in Jamf Pro and have available titles
319
+ #
320
+ # @param val [Object] The value to validate
321
+ #
322
+ # @return [Pathname] The valid value
323
+ ##########################
324
+ def validate_patch_source(val)
325
+ val = val.to_s
326
+ return val if jamf_patch_sources_with_available_titles.include? val
327
+
328
+ raise_invalid_data_error val, TITLE_ATTRS[:patch_source][:invalid_msg]
329
+ end
330
+
331
+ # validate a patch title id.
332
+ #
333
+ # @param val [Object] The value to validate
334
+ #
335
+ # @return [Pathname] The valid value
336
+ ##########################
337
+ def validate_title_id(val)
338
+ val = val.to_s
339
+ return val if jamf_available_titles.any? { |t| t[:name_id] == val }
340
+
341
+ raise_invalid_data_error val, TITLE_ATTRS[:title_id][:invalid_msg]
342
+ end
343
+
299
344
  # validate a title uninstall script:
300
345
  # - a path to a script which must start with '#!'
301
346
  # OR
@@ -525,9 +570,10 @@ module Xolo
525
570
  ##########################
526
571
  def validate_pkg_to_upload(val)
527
572
  val = Pathname.new val.to_s.strip
573
+ show_debug "Validating pkg_to_upload: '#{val}'. Checking if file? #{val.file?}, readable? #{val.readable?}, extname '#{val.extname}'"
528
574
  return val if val.file? && val.readable? && (Xolo::OK_PKG_EXTS.include? val.extname)
529
575
 
530
- raise_invalid_data_error val, VERSION_ATTRS[:jamf_pkg_file][:invalid_msg]
576
+ raise_invalid_data_error val, VERSION_ATTRS[:pkg_to_upload][:invalid_msg]
531
577
  end
532
578
 
533
579
  # Version Attributes
@@ -620,7 +666,7 @@ module Xolo
620
666
  val = val.to_s.strip
621
667
  return val if val =~ /\A\S+@\S+\.\S+\z/
622
668
 
623
- raise_invalid_data_error val, VERSION_ATTRS[:contact_email][:invalid_msg]
669
+ raise_invalid_data_error val, TITLE_ATTRS[:contact_email][:invalid_msg]
624
670
  end
625
671
 
626
672
  # expand Xolo::Admin::Version::USE_TITLE_FOR_KILLAPP into the proper killall string
@@ -790,6 +836,8 @@ module Xolo
790
836
  def validate_internal_consistency(opts)
791
837
  return unless add_command? || edit_command?
792
838
 
839
+ show_debug "Validating internal consistency for opts: #{opts.to_h}"
840
+
793
841
  if version_command?
794
842
  validate_version_consistency opts
795
843
  elsif title_command?
@@ -806,6 +854,12 @@ module Xolo
806
854
  validate_min_os_and_max_os(opts)
807
855
  end
808
856
 
857
+ # given our current options, are we working with a subscribed or managed title?
858
+ #
859
+ def current_title_type(opts)
860
+ opts[:subscribed] ? Xolo::SUBSCRIBED : Xolo::MANAGED
861
+ end
862
+
809
863
  # @param opts [OpenStruct] the current options
810
864
  #
811
865
  # @return [void]
@@ -814,86 +868,175 @@ module Xolo
814
868
  # if we are just listing the versions, nothing to do
815
869
  return if cli_cmd.command == Xolo::Admin::Options::LIST_VERSIONS_CMD
816
870
 
817
- # order of these matters
818
- validate_scope_targets_and_exclusions(opts)
819
- validate_title_consistency_app_and_script(opts)
871
+ # order of these matters in some places
872
+
873
+ #### Sub vs Managed
874
+
875
+ # complain about using options not meant for the chosen type
876
+ # e.g. can't use display name for subscribed titles
877
+ validate_subbed_v_managed(opts)
878
+
879
+ #### Subscribed Title Validations ####
880
+
881
+ # if subscribed, both patch_source and title_id must be given
882
+ validate_title_consistency_patch_source_and_title_id(opts)
883
+
884
+ # if subscribed, the title_id must exist in the patch_source
885
+ validate_title_consistency_title_id_exists(opts)
886
+
887
+ #### Managed Title Validations ####
888
+
889
+ # if managed, must provide display name and publisher, but not allowed if subscribing
890
+ validate_title_consistency_display_name_and_publisher(opts)
891
+
892
+ # if managed must use version_script, or app
820
893
  validate_title_consistency_app_or_script(opts)
894
+
895
+ # if managed, can't have both app and version_script
896
+ validate_title_consistency_app_and_script(opts)
897
+
898
+ # if managed and using app_name & bundle id, must have both
821
899
  validate_title_consistency_app_name_and_id(opts)
900
+
901
+ ### General Validations ###
902
+
903
+ # scope targets and exclusions can't overlap
904
+ validate_scope_targets_and_exclusions(opts)
905
+
906
+ # if uninstall script, can't have uninstall ids, and vice versa
822
907
  validate_title_consistency_uninstall(opts)
908
+
909
+ # if expiration > 0, must have at least one expiration path
823
910
  validate_title_consistency_expire_paths(opts)
911
+
912
+ # if targeting 'all' can't be in self-service
824
913
  validate_title_consistency_no_all_in_ssvc(opts)
914
+
915
+ # if self-service category given, must be in self-service
825
916
  validate_title_consistency_ssvc_needs_category(opts)
826
917
  end # title_consistency(opts)
827
918
 
828
- # groups that will be scope targets (pilot or release) cannot
829
- # also be in the exclusions.
919
+ # Complain about using options not meant for the chosen title type
920
+ def validate_subbed_v_managed(opts)
921
+ bad_opts = []
922
+ ttl_type = current_title_type(opts)
923
+
924
+ Xolo::Admin::Title.cli_opts.each do |key, deets|
925
+ next unless opts[key]
926
+ next unless deets[:title_type]
927
+ next if deets[:title_type] == ttl_type
928
+
929
+ # ignore values not used for this command type
930
+ next if add_command? && deets[:edit_only]
931
+ next if edit_command? && deets[:add_only]
932
+
933
+ # ignore values not given on the commandline
934
+ next if !walkthru? && !opts["#{key}_given"]
935
+
936
+ # display_name is stored for subscribed titles, but only editable for managed
937
+ next if key == :display_name && ttl_type == Xolo::SUBSCRIBED
938
+
939
+ bad_opts << (walkthru? ? deets[:label] : "--#{key.to_s.gsub('_', '-')}")
940
+ end
941
+ return if bad_opts.empty?
942
+
943
+ raise_consistency_error "Cannot be used with a #{ttl_type} title: #{bad_opts.join(', ')}"
944
+ end
945
+
946
+ # If subscribing to a title both patch_source and title_id must be given
830
947
  #
831
948
  # @param opts [OpenStruct] the current options
832
949
  #
833
950
  # @return [void]
834
951
  #######
835
- def validate_scope_targets_and_exclusions(opts)
836
- # require 'pp'
837
- # puts 'Opts Are:'
838
- # pp opts.to_h
839
- # pp caller
952
+ def validate_title_consistency_patch_source_and_title_id(opts)
953
+ return if current_title_type(opts) == Xolo::MANAGED
954
+ return if opts[:title_id] && opts[:patch_source]
840
955
 
841
- if title_command?
842
- excls = opts.excluded_groups
843
- tgts = opts.release_groups
844
- tgt_type = :release
845
- elsif version_command?
846
- @title_for_version_validation ||= Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
847
- excls = @title_for_version_validation.excluded_groups
848
- tgts = opts.pilot_groups
849
- tgt_type = :pilot
850
- else
851
- excls = nil
852
- tgts = nil
956
+ msg =
957
+ if walkthru?
958
+ 'To subscribe to a title, both Patch Source and Title ID must be given.'
959
+ else
960
+ 'To subscribe to a title, both --patch-source and --title-id must be given.'
961
+ end
962
+ raise_consistency_error msg
963
+ end
964
+
965
+ # If subscribing to a title, the title_id must exist in the given patch_source
966
+ #
967
+ # @param opts [OpenStruct] the current options
968
+ #
969
+ # @return [void]
970
+ #######
971
+ def validate_title_consistency_title_id_exists(opts)
972
+ return unless cli_cmd.command == Xolo::Admin::Options::ADD_TITLE_CMD
973
+ return if current_title_type(opts) == Xolo::MANAGED
974
+ return unless opts[:title_id] && opts[:patch_source]
975
+ return if jamf_available_titles.any? do |t|
976
+ t[:name_id] == opts[:title_id] && t[:source_name] == opts[:patch_source]
853
977
  end
854
- return unless excls && tgts
855
978
 
856
- in_both = excls & tgts
857
- return if in_both.empty?
979
+ msg =
980
+ if walkthru?
981
+ 'The given Title ID does not exist in the given Patch Source.'
982
+ else
983
+ 'The given --title-id does not exist in the given --patch-source.'
984
+ end
985
+ raise_consistency_error msg
986
+ end
858
987
 
859
- raise_consistency_error "These groups are in both #{tgt_type}_groups and the title's excluded_groups: '#{in_both.join "', '"}'"
988
+ # If managing this title, display_name and publisher must be given
989
+ #################
990
+ def validate_title_consistency_display_name_and_publisher(opts)
991
+ return if current_title_type(opts) == Xolo::SUBSCRIBED
992
+ return if opts[:display_name] && opts[:publisher]
993
+
994
+ msg =
995
+ if walkthru?
996
+ 'Display Name and Publisher must both be given for a managed title.'
997
+ else
998
+ '--display-name and --publisher must both be given for a managed title.'
999
+ end
1000
+ raise_consistency_error msg
860
1001
  end
861
1002
 
862
- # if app_name or app_bundle_id is given, can't use --version-script
1003
+ # if managed must use version_script, or app
863
1004
  #
864
1005
  # @param opts [OpenStruct] the current options
865
1006
  #
866
1007
  # @return [void]
867
1008
  #######
868
- def validate_title_consistency_app_and_script(opts)
869
- return unless opts[:version_script] && (opts[:app_bundle_id] || opts[:app_name])
1009
+ def validate_title_consistency_app_or_script(opts)
1010
+ return if current_title_type(opts) == Xolo::SUBSCRIBED
1011
+ return if opts[:version_script] || (opts[:app_bundle_id] && opts[:app_name])
870
1012
 
871
1013
  msg =
872
1014
  if walkthru?
873
- 'Version Script cannot be used with App Name & App Bundle ID'
1015
+ 'Managed titles must provide Version Script OR App Name & App Bundle ID'
874
1016
  else
875
- '--version-script cannot be used with --app-name & --app-bundle-id'
1017
+ 'Managed titles must provide --version-script OR --app-name & --app-bundle-id'
876
1018
  end
877
1019
 
878
1020
  raise_consistency_error msg
879
1021
  end
880
1022
 
881
- # but either version_script or appname and bundle id must be given
1023
+ # if app_name or app_bundle_id is given, can't use --version-script, and vice versa
882
1024
  #
883
1025
  # @param opts [OpenStruct] the current options
884
1026
  #
885
1027
  # @return [void]
886
1028
  #######
887
- def validate_title_consistency_app_or_script(opts)
888
- return if opts[:version_script]
889
- return if opts[:app_name] || opts[:app_bundle_id]
1029
+ def validate_title_consistency_app_and_script(opts)
1030
+ return if current_title_type(opts) == Xolo::SUBSCRIBED
1031
+ return unless opts[:version_script] && (opts[:app_bundle_id] || opts[:app_name])
890
1032
 
891
1033
  msg =
892
1034
  if walkthru?
893
- 'Either App Name & App Bundle ID. or Version Script must be given.'
1035
+ 'Version Script cannot be used with App Name & App Bundle ID'
894
1036
  else
895
- 'Must provide either --app-name & --app-bundle-id OR --version-script'
1037
+ '--version-script cannot be used with --app-name & --app-bundle-id'
896
1038
  end
1039
+
897
1040
  raise_consistency_error msg
898
1041
  end
899
1042
 
@@ -904,6 +1047,7 @@ module Xolo
904
1047
  # @return [void]
905
1048
  #######
906
1049
  def validate_title_consistency_app_name_and_id(opts)
1050
+ return if current_title_type(opts) == Xolo::SUBSCRIBED
907
1051
  return if opts[:version_script]
908
1052
  return if opts[:app_name] && opts[:app_bundle_id]
909
1053
 
@@ -916,6 +1060,40 @@ module Xolo
916
1060
  raise_consistency_error msg
917
1061
  end
918
1062
 
1063
+ # groups that will be scope targets (pilot or release) cannot
1064
+ # also be in the exclusions.
1065
+ #
1066
+ # @param opts [OpenStruct] the current options
1067
+ #
1068
+ # @return [void]
1069
+ #######
1070
+ def validate_scope_targets_and_exclusions(opts)
1071
+ # require 'pp'
1072
+ # puts 'Opts Are:'
1073
+ # pp opts.to_h
1074
+ # pp caller
1075
+
1076
+ if title_command?
1077
+ excls = opts.excluded_groups
1078
+ tgts = opts.release_groups
1079
+ tgt_type = :release
1080
+ elsif version_command?
1081
+ @title_for_version_validation ||= Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
1082
+ excls = @title_for_version_validation.excluded_groups
1083
+ tgts = opts.pilot_groups
1084
+ tgt_type = :pilot
1085
+ else
1086
+ excls = nil
1087
+ tgts = nil
1088
+ end
1089
+ return unless excls && tgts
1090
+
1091
+ in_both = excls & tgts
1092
+ return if in_both.empty?
1093
+
1094
+ raise_consistency_error "These groups are in both #{tgt_type}_groups and the title's excluded_groups: '#{in_both.join "', '"}'"
1095
+ end
1096
+
919
1097
  # if expiration is > 0, there must be at least one expiration path
920
1098
  #
921
1099
  # @param opts [OpenStruct] the current options
@@ -975,7 +1153,7 @@ module Xolo
975
1153
  if walkthru?
976
1154
  "Cannot be in Self Service when Target Group is '#{Xolo::TARGET_ALL}'"
977
1155
  else
978
- "--self-service cannot be used when --target-groups contains '#{Xolo::TARGET_ALL}'"
1156
+ "--self-service cannot be used when --release-groups contains '#{Xolo::TARGET_ALL}'"
979
1157
  end
980
1158
  raise_consistency_error msg
981
1159
  end
@@ -86,7 +86,11 @@ module Xolo
86
86
  ####################
87
87
  def self.fetch(title, version, cnx)
88
88
  resp = cnx.get server_route(title, version)
89
- new resp.body
89
+ vers_obj = new resp.body
90
+ vers_obj.cnx = cnx
91
+ vers_obj
92
+ rescue Faraday::ResourceNotFound
93
+ raise Xolo::NoSuchItemError, "No such version '#{version}'"
90
94
  end
91
95
 
92
96
  # Deploy a version to desired computers and groups via MDM
@@ -121,6 +125,9 @@ module Xolo
121
125
  ######################
122
126
  ######################
123
127
 
128
+ # the server connection used to fetch this version
129
+ attr_accessor :cnx
130
+
124
131
  # Constructor
125
132
  ######################
126
133
  ######################
@@ -129,13 +136,31 @@ module Xolo
129
136
  #############################
130
137
  #############################
131
138
 
132
- # The server route for this version, after it exists on the server
139
+ # TODO:
140
+ # when instantiating a title or version object,
141
+ # embed the connection used to fetch it in a @cnx attribute/instance variable
142
+ # and use that for all future interactions with the server for that object.
143
+ #
144
+ # Then also use that to implement the method below, fetching the title object
145
+ # for this version when desired.
146
+ #
147
+ # Then we can use THAT to know if this title is subscribed or not
148
+ # which affects how we deal with adding (we can't) or editing
149
+ #
150
+ # Also TODO: implement a way to trigger a subcribed 'add-title'
151
+ # in case the xolo server misses a webhook event
152
+
153
+ ################
154
+ # def title_object(refresh: false, cnx:
155
+ # @title_object = nil if refresh
156
+ # @title_object ||= Xolo::Admin::Title.fetch(title, cnx)
157
+ # end
133
158
 
134
159
  # Add this version to the server
135
160
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
136
161
  # @return [Hash] the response from the server
137
162
  ####################
138
- def add(cnx)
163
+ def add(cnx = self.cnx)
139
164
  resp = cnx.post self.class.server_route(title), to_h
140
165
  resp.body
141
166
  end
@@ -144,7 +169,7 @@ module Xolo
144
169
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
145
170
  # @return [Hash] the response from the server
146
171
  ####################
147
- def update(cnx)
172
+ def update(cnx = self.cnx)
148
173
  resp = cnx.put self.class.server_route(title, version), to_h
149
174
  resp.body
150
175
  end
@@ -153,7 +178,7 @@ module Xolo
153
178
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
154
179
  # @return [Hash] the response body from the server
155
180
  ####################
156
- def repair(cnx)
181
+ def repair(cnx = self.cnx)
157
182
  resp = cnx.post "#{self.class.server_route(title, version)}/repair"
158
183
  resp.body
159
184
  end
@@ -162,7 +187,7 @@ module Xolo
162
187
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
163
188
  # @return [Hash] the response from the server
164
189
  ####################
165
- def delete(cnx)
190
+ def delete(cnx = self.cnx)
166
191
  self.class.delete title, version, cnx
167
192
  # already returns resp.body
168
193
  end
@@ -171,14 +196,14 @@ module Xolo
171
196
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
172
197
  # @return [Hash{String => String}] page_name => url
173
198
  ####################
174
- def gui_urls(cnx)
199
+ def gui_urls(cnx = self.cnx)
175
200
  resp = cnx.get "#{self.class.server_route(title, version)}/urls"
176
201
  resp.body
177
202
  end
178
203
 
179
- # Upload a .pkg (or zipped bundle pkg) for this version
180
- # At this point, the jamf_pkg_file attribute
181
- # will containt the local file path.
204
+ # Upload a .pkg for this version
205
+ # At this point, the pkg_to_upload attribute
206
+ # will contain the local file path.
182
207
  #
183
208
  # @param upload_cnx [Xolo::Admin::Connection] The server connection
184
209
  #
@@ -190,13 +215,6 @@ module Xolo
190
215
  # route = "#{UPLOAD_PKG_ROUTE}/#{title}/#{version}"
191
216
  route = "#{self.class.server_route(title, version)}/#{UPLOAD_PKG_ROUTE}"
192
217
 
193
- # TODO: Update this to the more modern correct class
194
- # upfile = Faraday::UploadIO.new(
195
- # pkg_to_upload.to_s,
196
- # 'application/octet-stream',
197
- # pkg_to_upload.basename.to_s
198
- # )
199
-
200
218
  upfile = Faraday::Multipart::FilePart.new(pkg_to_upload.expand_path.to_s, 'application/octet-stream')
201
219
 
202
220
  content = { file: upfile }
@@ -209,11 +227,30 @@ module Xolo
209
227
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
210
228
  # @return [Array<Hash>] Data for each computer with this version of this title installed
211
229
  ##################################
212
- def patch_report_data(cnx)
230
+ def patch_report_data(cnx = self.cnx)
213
231
  resp = cnx.get "#{self.class.server_route(title, version)}/patch_report"
214
232
  resp.body
215
233
  end
216
234
 
235
+ # @return [Xolo::Admin::Title] the title for this version
236
+ ################
237
+ def title_object(cnx = self.cnx, refresh: false)
238
+ @title_object = nil if refresh
239
+ @title_object ||= Xolo::Admin::Title.fetch(title, cnx)
240
+ end
241
+
242
+ # @return [Boolean] whether this version is from a subscribed title or not
243
+ #############
244
+ def subscribed?
245
+ title_object.subscribed?
246
+ end
247
+
248
+ # @return [Boolean] whether this version is from a managed title or not
249
+ #############
250
+ def managed?
251
+ title_object.managed?
252
+ end
253
+
217
254
  end # class Title
218
255
 
219
256
  end # module Admin