xolo-admin 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +177 -0
- data/README.md +5 -0
- data/bin/xadm +114 -0
- data/lib/xolo/admin/command_line.rb +432 -0
- data/lib/xolo/admin/configuration.rb +196 -0
- data/lib/xolo/admin/connection.rb +191 -0
- data/lib/xolo/admin/cookie_jar.rb +81 -0
- data/lib/xolo/admin/credentials.rb +212 -0
- data/lib/xolo/admin/highline_terminal.rb +81 -0
- data/lib/xolo/admin/interactive.rb +762 -0
- data/lib/xolo/admin/jamf_pro.rb +75 -0
- data/lib/xolo/admin/options.rb +1139 -0
- data/lib/xolo/admin/processing.rb +1329 -0
- data/lib/xolo/admin/progress_history.rb +117 -0
- data/lib/xolo/admin/title.rb +285 -0
- data/lib/xolo/admin/title_editor.rb +57 -0
- data/lib/xolo/admin/validate.rb +1032 -0
- data/lib/xolo/admin/version.rb +221 -0
- data/lib/xolo/admin.rb +139 -0
- data/lib/xolo-admin.rb +8 -0
- metadata +139 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Admin
|
|
14
|
+
|
|
15
|
+
# Storage and access to a history of progress-data from
|
|
16
|
+
# long-running Xolo server processes.
|
|
17
|
+
#
|
|
18
|
+
# E.g.. when you run 'xadm delete-title' you'll get a live
|
|
19
|
+
# progress updates of everything happening. That log is stored for
|
|
20
|
+
# a while (3 days) on the server.
|
|
21
|
+
#
|
|
22
|
+
# Later, esp if you used --quiet and didn't see the progress or any
|
|
23
|
+
# errors initially
|
|
24
|
+
# you'll be able to re-view the progress log, if it still exists
|
|
25
|
+
# on the server
|
|
26
|
+
#
|
|
27
|
+
module ProgressHistory
|
|
28
|
+
|
|
29
|
+
# Constants
|
|
30
|
+
##########################
|
|
31
|
+
##########################
|
|
32
|
+
|
|
33
|
+
APP_SUPPORT_DIR = '~/Library/Application Support/xadm/'
|
|
34
|
+
PROGRESS_HISTORY_FILENAME = 'com.pixar.xolo.admin.progress_history.yaml'
|
|
35
|
+
|
|
36
|
+
# prog files on the server last 3 days, add an extra to
|
|
37
|
+
# account for timing of daily cleanup on the server.
|
|
38
|
+
# if the file is already gone from the server, we'll tell
|
|
39
|
+
# the user
|
|
40
|
+
PROGRESS_FILE_LIFETIME = 4 * 24 * 3600
|
|
41
|
+
|
|
42
|
+
# Module Methods
|
|
43
|
+
##########################
|
|
44
|
+
##########################
|
|
45
|
+
|
|
46
|
+
# when this module is included
|
|
47
|
+
def self.included(includer)
|
|
48
|
+
Xolo.verbose_include includer, self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# when this module is extended
|
|
52
|
+
def self.extended(extender)
|
|
53
|
+
Xolo.verbose_extend extender, self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Instance Methods
|
|
57
|
+
##########################
|
|
58
|
+
##########################
|
|
59
|
+
|
|
60
|
+
# @return [Pathname] The expanded path tot he prog. history dir for this user
|
|
61
|
+
###########################
|
|
62
|
+
def app_support_dir
|
|
63
|
+
return @app_support_dir if @app_support_dir
|
|
64
|
+
|
|
65
|
+
@app_support_dir = Pathname.new(APP_SUPPORT_DIR).expand_path
|
|
66
|
+
@app_support_dir.mkpath
|
|
67
|
+
@app_support_dir
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Pathname] The prog. history file for this user
|
|
71
|
+
###########################
|
|
72
|
+
def progress_history_file
|
|
73
|
+
@progress_history_file ||= (app_support_dir + PROGRESS_HISTORY_FILENAME)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The current history of progress streams for this user
|
|
77
|
+
# keys are Time objects when the entry was created
|
|
78
|
+
# values are sub-hashes with :command, and :url keys
|
|
79
|
+
# the command being the xadm command, e.g. 'delete-version'
|
|
80
|
+
# and the URL being the url to the progress stream file on the server.
|
|
81
|
+
#
|
|
82
|
+
# before returning the hash, any expired entries are removed
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash] the current progress history for this user
|
|
85
|
+
###################
|
|
86
|
+
def progress_history
|
|
87
|
+
progress_history_file.pix_touch
|
|
88
|
+
history = YAML.load progress_history_file.read
|
|
89
|
+
history ||= {}
|
|
90
|
+
|
|
91
|
+
now = Time.now
|
|
92
|
+
history.delete_if { |k, _v| now - k > PROGRESS_FILE_LIFETIME }
|
|
93
|
+
|
|
94
|
+
history
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Add an entry to the progress history
|
|
98
|
+
#
|
|
99
|
+
# @prarm url_path [String] the xolo server path to the progress file.
|
|
100
|
+
#
|
|
101
|
+
# @return [void]
|
|
102
|
+
######################
|
|
103
|
+
def add_progress_history_entry(url_path)
|
|
104
|
+
history = progress_history
|
|
105
|
+
history[Time.now] = {
|
|
106
|
+
command: cli_cmd.command,
|
|
107
|
+
url_path: url_path
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
progress_history_file.pix_atomic_write YAML.dump(history)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end # module
|
|
114
|
+
|
|
115
|
+
end # module Admin
|
|
116
|
+
|
|
117
|
+
end # module Xolo
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Admin
|
|
14
|
+
|
|
15
|
+
# A title used by xadm.
|
|
16
|
+
#
|
|
17
|
+
# These are instantiated with data from the server
|
|
18
|
+
# (for existing Titles) or from the xadm CLI opts
|
|
19
|
+
# or walkthru process.
|
|
20
|
+
#
|
|
21
|
+
# This class also defines how xadm communicates
|
|
22
|
+
# title data to and from the server.
|
|
23
|
+
class Title < Xolo::Core::BaseClasses::Title
|
|
24
|
+
|
|
25
|
+
# Constants
|
|
26
|
+
#############################
|
|
27
|
+
#############################
|
|
28
|
+
|
|
29
|
+
# This is the server path for dealing with titles
|
|
30
|
+
# POST to add a new one
|
|
31
|
+
# GET to get a list of titles
|
|
32
|
+
# GET .../<title> to get the data for a single title
|
|
33
|
+
# PUT .../<title> to update a title with new data
|
|
34
|
+
# DELETE .../<title> to delete a title and its version
|
|
35
|
+
SERVER_ROUTE = '/titles'
|
|
36
|
+
|
|
37
|
+
UPLOAD_ICON_ROUTE = 'ssvc-icon'
|
|
38
|
+
|
|
39
|
+
TARGET_TITLE_PLACEHOLDER = 'TARGET_TITLE_PH'
|
|
40
|
+
|
|
41
|
+
# Class Methods
|
|
42
|
+
#############################
|
|
43
|
+
#############################
|
|
44
|
+
|
|
45
|
+
# @return [Hash{Symbol: Hash}] The ATTRIBUTES that are available as CLI & walkthru options
|
|
46
|
+
def self.cli_opts
|
|
47
|
+
@cli_opts ||= ATTRIBUTES.select { |_k, v| v[:cli] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Array<String>] The currently known titles names on the server
|
|
51
|
+
#############################
|
|
52
|
+
def self.all_titles(cnx)
|
|
53
|
+
resp = cnx.get SERVER_ROUTE
|
|
54
|
+
resp.body.map { |t| t[:title] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Array<Xolo::Admin::Title>] The currently known titles on the server
|
|
58
|
+
#############################
|
|
59
|
+
def self.all_title_objects(cnx)
|
|
60
|
+
resp = cnx.get SERVER_ROUTE
|
|
61
|
+
resp.body.map { |td| Xolo::Admin::Title.new td }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Does a title exist on the server?
|
|
65
|
+
# @param title [String] the title
|
|
66
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
#############################
|
|
69
|
+
def self.exist?(title, cnx)
|
|
70
|
+
all_titles(cnx).include? title
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Fetch a title from the server
|
|
74
|
+
# @param title [String] the title to fetch
|
|
75
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
76
|
+
# @return [Xolo::Admin::Title]
|
|
77
|
+
####################
|
|
78
|
+
def self.fetch(title, cnx)
|
|
79
|
+
resp = cnx.get "#{SERVER_ROUTE}/#{title}"
|
|
80
|
+
|
|
81
|
+
new resp.body
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Delete a title from the server
|
|
85
|
+
# @param title [String] the title to delete
|
|
86
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
87
|
+
# @return [Hash] the response data
|
|
88
|
+
####################
|
|
89
|
+
def self.delete(title, cnx)
|
|
90
|
+
resp = cnx.delete "#{SERVER_ROUTE}/#{title}"
|
|
91
|
+
resp.body
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# the latest version of a title in Xolo
|
|
95
|
+
# @param title [String] the title we care about
|
|
96
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
97
|
+
# @return [void]
|
|
98
|
+
####################
|
|
99
|
+
def self.latest_version(title, cnx)
|
|
100
|
+
resp = cnx.get "#{SERVER_ROUTE}/#{title}"
|
|
101
|
+
resp.body[:version_order].first
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Is the current admin allowed to set a title's release groups to 'all'?
|
|
105
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
####################
|
|
108
|
+
def self.release_to_all_allowed?(cnx)
|
|
109
|
+
resp = cnx.get '/auth/release_to_all_allowed'
|
|
110
|
+
resp.body
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Attributes
|
|
114
|
+
######################
|
|
115
|
+
######################
|
|
116
|
+
|
|
117
|
+
# Constructor
|
|
118
|
+
######################
|
|
119
|
+
######################
|
|
120
|
+
|
|
121
|
+
# Read in the contents of any version script given
|
|
122
|
+
def initialize(data_hash)
|
|
123
|
+
super
|
|
124
|
+
# @self_service_icon = nil if @self_service_icon == Xolo::ITEM_UPLOADED
|
|
125
|
+
|
|
126
|
+
return unless version_script
|
|
127
|
+
return if version_script == Xolo::ITEM_UPLOADED
|
|
128
|
+
|
|
129
|
+
@version_script = version_script.read if version_script.respond_to?(:read)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Instance Methods
|
|
133
|
+
#############################
|
|
134
|
+
#############################
|
|
135
|
+
|
|
136
|
+
# Add this title to the server
|
|
137
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
138
|
+
# @return [Hash] the response body from the server
|
|
139
|
+
####################
|
|
140
|
+
def add(cnx)
|
|
141
|
+
resp = cnx.post SERVER_ROUTE, to_h
|
|
142
|
+
resp.body
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Update this title to the server
|
|
146
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
147
|
+
# @return [Hash] the response body from the server
|
|
148
|
+
####################
|
|
149
|
+
def update(cnx)
|
|
150
|
+
resp = cnx.put "#{SERVER_ROUTE}/#{title}", to_h
|
|
151
|
+
resp.body
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Release a version of this title.
|
|
155
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
156
|
+
# @param version [String] the version to release
|
|
157
|
+
# @return [Hash] the response body from the server
|
|
158
|
+
####################
|
|
159
|
+
def release(cnx, version:)
|
|
160
|
+
resp = cnx.patch "#{SERVER_ROUTE}/#{title}/release/#{version}", {}
|
|
161
|
+
resp.body
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Repair this title, and optionally all its versions
|
|
165
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
166
|
+
# @param versions [Boolean] if true, repair all versions of this title
|
|
167
|
+
# @return [Hash] the response body from the server
|
|
168
|
+
####################
|
|
169
|
+
def repair(cnx, versions: false)
|
|
170
|
+
resp = cnx.post "#{SERVER_ROUTE}/#{title}/repair", { repair_versions: versions }
|
|
171
|
+
resp.body
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Delete this title from the server
|
|
175
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
176
|
+
# @return [Hash] the response data
|
|
177
|
+
####################
|
|
178
|
+
def delete(cnx)
|
|
179
|
+
self.class.delete title, cnx
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Freeze the one or more computers for this title
|
|
183
|
+
# @param computers [Array<String>] the computers to freeze
|
|
184
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
185
|
+
# @return [Hash] the response data
|
|
186
|
+
####################
|
|
187
|
+
def freeze(targets, users = false, cnx)
|
|
188
|
+
data = { targets: targets, users: users }
|
|
189
|
+
resp = cnx.put "#{SERVER_ROUTE}/#{title}/freeze", data
|
|
190
|
+
resp.body
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Thaw the one or more computers for this title
|
|
194
|
+
# @param computers [Array<String>] the computers to freeze
|
|
195
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
196
|
+
# @return [Hash] the response data
|
|
197
|
+
####################
|
|
198
|
+
def thaw(targets, users = false, cnx)
|
|
199
|
+
data = { targets: targets, users: users }
|
|
200
|
+
resp = cnx.put "#{SERVER_ROUTE}/#{title}/thaw", data
|
|
201
|
+
resp.body
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Fetch the frozen computers for this title
|
|
205
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
206
|
+
# @return [Hash{String => String}] computer name => user name
|
|
207
|
+
####################
|
|
208
|
+
def frozen(cnx)
|
|
209
|
+
resp = cnx.get "#{SERVER_ROUTE}/#{title}/frozen"
|
|
210
|
+
resp.body
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Fetch a hash of URLs for the GUI pages for this title
|
|
214
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
215
|
+
# @return [Hash{String => String}] page_name => url
|
|
216
|
+
####################
|
|
217
|
+
def gui_urls(cnx)
|
|
218
|
+
resp = cnx.get "#{SERVER_ROUTE}/#{title}/urls"
|
|
219
|
+
resp.body
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The change log is a list of hashes, each with keys:
|
|
223
|
+
# :time, :admin, :ipaddr, :version (may be nil), :action
|
|
224
|
+
#
|
|
225
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
226
|
+
# @return [Array<Hash>] The change log for this title
|
|
227
|
+
####################
|
|
228
|
+
def changelog(cnx)
|
|
229
|
+
resp = cnx.get "#{SERVER_ROUTE}/#{title}/changelog"
|
|
230
|
+
resp.body
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Upload an icon for self service.
|
|
234
|
+
# At this point, the self_service_icon attribute
|
|
235
|
+
# should contain the local file path.
|
|
236
|
+
#
|
|
237
|
+
# @param upload_cnx [Xolo::Admin::Connection] The server connection
|
|
238
|
+
#
|
|
239
|
+
# @return [Faraday::Response] The server response
|
|
240
|
+
##################################
|
|
241
|
+
def upload_self_service_icon(upload_cnx)
|
|
242
|
+
return unless self_service_icon.is_a? Pathname
|
|
243
|
+
|
|
244
|
+
unless self_service_icon.readable?
|
|
245
|
+
raise Xolo::NoSuchItemError,
|
|
246
|
+
"Can't upload self service icon '#{self_service_icon}': file doesn't exist or is not readable"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# upfile = Faraday::UploadIO.new(
|
|
250
|
+
# self_service_icon.to_s,
|
|
251
|
+
# 'application/octet-stream',
|
|
252
|
+
# self_service_icon.basename.to_s
|
|
253
|
+
# )
|
|
254
|
+
|
|
255
|
+
mimetype = `/usr/bin/file --brief --mime-type #{Shellwords.escape self_service_icon.expand_path.to_s}`.chomp
|
|
256
|
+
upfile = Faraday::Multipart::FilePart.new(self_service_icon.expand_path.to_s, mimetype)
|
|
257
|
+
content = { file: upfile }
|
|
258
|
+
# route = "#{UPLOAD_ICON_ROUTE}/#{title}"
|
|
259
|
+
route = "#{SERVER_ROUTE}/#{title}/#{UPLOAD_ICON_ROUTE}"
|
|
260
|
+
|
|
261
|
+
upload_cnx.post(route) { |req| req.body = content }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Get the patch report data for this title
|
|
265
|
+
# It's the JPAPI report data with each hash having a frozen: key added
|
|
266
|
+
#
|
|
267
|
+
# @param cnx [Faraday::Connection] The connection to use, must be logged in already
|
|
268
|
+
# @return [Array<Hash>] Data for each computer with any version of this title installed
|
|
269
|
+
##################################
|
|
270
|
+
def patch_report_data(cnx)
|
|
271
|
+
resp = cnx.get "#{SERVER_ROUTE}/#{title}/patch_report"
|
|
272
|
+
resp.body
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Add more data to our hash
|
|
276
|
+
###########################
|
|
277
|
+
def to_h
|
|
278
|
+
super
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
end # class Title
|
|
282
|
+
|
|
283
|
+
end # module Admin
|
|
284
|
+
|
|
285
|
+
end # module Xolo
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Admin
|
|
14
|
+
|
|
15
|
+
# Methods that process the xadm commands and their options
|
|
16
|
+
#
|
|
17
|
+
module TitleEditor
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
##########################
|
|
21
|
+
##########################
|
|
22
|
+
|
|
23
|
+
TITLE_EDITOR_ROUTE_BASE = '/title-editor'
|
|
24
|
+
|
|
25
|
+
# Xolo server route to the list of titles
|
|
26
|
+
TITLES_ROUTE = "#{TITLE_EDITOR_ROUTE_BASE}/titles"
|
|
27
|
+
|
|
28
|
+
# Module Methods
|
|
29
|
+
##########################
|
|
30
|
+
##########################
|
|
31
|
+
|
|
32
|
+
# when this module is included
|
|
33
|
+
def self.included(includer)
|
|
34
|
+
Xolo.verbose_include includer, self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# when this module is extended
|
|
38
|
+
def self.extended(extender)
|
|
39
|
+
Xolo.verbose_extend extender, self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Instance Methods
|
|
43
|
+
##########################
|
|
44
|
+
##########################
|
|
45
|
+
|
|
46
|
+
# Perhaps not needed for anything, but used for initial connection testing
|
|
47
|
+
# @return [Array<String>] the titles of all Title objects in the Title Editor
|
|
48
|
+
#######################
|
|
49
|
+
def ted_titles
|
|
50
|
+
server_cnx.get(TITLES_ROUTE).body
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end # module
|
|
54
|
+
|
|
55
|
+
end # module Admin
|
|
56
|
+
|
|
57
|
+
end # module Xolo
|