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.
@@ -0,0 +1,221 @@
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 version/patch as used by xadm.
16
+ # This adds cli and walkthru UI, as well as
17
+ # an interface to the Xolo Server for Title
18
+ # objects.
19
+ class Version < Xolo::Core::BaseClasses::Version
20
+
21
+ # This is the server path for dealing with titles
22
+ # POST to add a new one
23
+ # GET to get a list of versions for a title
24
+ # GET .../<version> to get the data for a single version
25
+ # PUT .../<version> to update a version with new data
26
+ # DELETE .../<version> to delete a version from the title
27
+ SERVER_ROUTE = "/titles/#{Xolo::Admin::Title::TARGET_TITLE_PLACEHOLDER}/versions"
28
+
29
+ # Server route for uploading packages
30
+ UPLOAD_PKG_ROUTE = 'pkg'
31
+
32
+ # Modification of the ATTRIBUTES constant for how they are handled
33
+ # in the admin app
34
+ ATTRIBUTES[:min_os][:default] = proc { Xolo::Admin::Options.default_min_os }
35
+
36
+ # Class Methods
37
+ #############################
38
+ #############################
39
+
40
+ # @return [Hash{Symbol: Hash}] The ATTRIBUTES that are available as CLI & walkthru options
41
+ def self.cli_opts
42
+ @cli_opts ||= ATTRIBUTES.select { |_k, v| v[:cli] }
43
+ end
44
+
45
+ # get the server route to a specific version (or the version list) for a title
46
+ # @param title [String] the title
47
+ # @param version [String] the version to fetch
48
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
49
+ # @return [Xolo::Admin::Title]
50
+ ####################
51
+ def self.server_route(title, version = nil)
52
+ route = SERVER_ROUTE.sub(Xolo::Admin::Title::TARGET_TITLE_PLACEHOLDER, title)
53
+ route << "/#{version}" if version
54
+ route
55
+ end
56
+
57
+ # @return [Array<String>] The currently known versions of a title on the server
58
+ #############################
59
+ def self.all_versions(title, cnx)
60
+ resp = cnx.get server_route(title)
61
+ resp.body
62
+ end
63
+
64
+ # @return [Array<Xolo::Admin::Version>] The currently known versions of a title on the server
65
+ #############################
66
+ def self.all_version_objects(title, cnx)
67
+ resp = cnx.get server_route(title)
68
+ resp.body.map { |vd| Xolo::Admin::Version.new vd }
69
+ end
70
+
71
+ # Does a version of a title exist on the server?
72
+ # @param title [String] the title
73
+ # @param version [String] the version
74
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
75
+ # @return [Boolean]
76
+ #############################
77
+ def self.exist?(title, version, cnx)
78
+ all_versions(title, cnx).include? version
79
+ end
80
+
81
+ # Fetch a version of a title from the server
82
+ # @param title [String] the title
83
+ # @param version [String] the version to fetch
84
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
85
+ # @return [Xolo::Admin::Title]
86
+ ####################
87
+ def self.fetch(title, version, cnx)
88
+ resp = cnx.get server_route(title, version)
89
+ new resp.body
90
+ end
91
+
92
+ # Deploy a version to desired computers and groups via MDM
93
+ #
94
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
95
+ # @param groups [Array<String, Integer>] The groups to deploy to
96
+ # @param computers [Array<String, Integer>] The computers to deploy to
97
+ #
98
+ # @return [Hash] The response from the server
99
+ ####################
100
+ def self.deploy(title, version, cnx, groups: [], computers: [])
101
+ raise ArgumentError, 'Must provide at least one group or computer' if groups.pix_empty? && computers.pix_empty?
102
+
103
+ route = "#{server_route(title, version)}/deploy"
104
+ content = { groups: groups, computers: computers }
105
+ resp = cnx.post(route) { |req| req.body = content }
106
+ resp.body
107
+ end
108
+
109
+ # Delete a version of a title from the server
110
+ # @param title [String] the title
111
+ # @param version [String] the version to delete
112
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
113
+ # @return [Hash] the response body, parsed JSON
114
+ ####################
115
+ def self.delete(title, version, cnx)
116
+ resp = cnx.delete server_route(title, version)
117
+ resp.body
118
+ end
119
+
120
+ # Attributes
121
+ ######################
122
+ ######################
123
+
124
+ # Constructor
125
+ ######################
126
+ ######################
127
+
128
+ # Instance Methods
129
+ #############################
130
+ #############################
131
+
132
+ # The server route for this version, after it exists on the server
133
+
134
+ # Add this version to the server
135
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
136
+ # @return [Hash] the response from the server
137
+ ####################
138
+ def add(cnx)
139
+ resp = cnx.post self.class.server_route(title), to_h
140
+ resp.body
141
+ end
142
+
143
+ # Update this version to the server
144
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
145
+ # @return [Hash] the response from the server
146
+ ####################
147
+ def update(cnx)
148
+ resp = cnx.put self.class.server_route(title, version), to_h
149
+ resp.body
150
+ end
151
+
152
+ # Repair this version
153
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
154
+ # @return [Hash] the response body from the server
155
+ ####################
156
+ def repair(cnx)
157
+ resp = cnx.post "#{self.class.server_route(title, version)}/repair"
158
+ resp.body
159
+ end
160
+
161
+ # Delete this title from the server
162
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
163
+ # @return [Hash] the response from the server
164
+ ####################
165
+ def delete(cnx)
166
+ self.class.delete title, version, cnx
167
+ # already returns resp.body
168
+ end
169
+
170
+ # Fetch a hash of URLs for the GUI pages for this title
171
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
172
+ # @return [Hash{String => String}] page_name => url
173
+ ####################
174
+ def gui_urls(cnx)
175
+ resp = cnx.get "#{self.class.server_route(title, version)}/urls"
176
+ resp.body
177
+ end
178
+
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.
182
+ #
183
+ # @param upload_cnx [Xolo::Admin::Connection] The server connection
184
+ #
185
+ # @return [Faraday::Response] The server response
186
+ ##################################
187
+ def upload_pkg(upload_cnx)
188
+ return unless pkg_to_upload.is_a? Pathname
189
+
190
+ # route = "#{UPLOAD_PKG_ROUTE}/#{title}/#{version}"
191
+ route = "#{self.class.server_route(title, version)}/#{UPLOAD_PKG_ROUTE}"
192
+
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
+ upfile = Faraday::Multipart::FilePart.new(pkg_to_upload.expand_path.to_s, 'application/octet-stream')
201
+
202
+ content = { file: upfile }
203
+ upload_cnx.post(route) { |req| req.body = content }
204
+ end
205
+
206
+ # Get the Patch Report data for this version
207
+ # It's the JPAPI report data with each hash having a frozen: key added
208
+ #
209
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
210
+ # @return [Array<Hash>] Data for each computer with this version of this title installed
211
+ ##################################
212
+ def patch_report_data(cnx)
213
+ resp = cnx.get "#{self.class.server_route(title, version)}/patch_report"
214
+ resp.body
215
+ end
216
+
217
+ end # class Title
218
+
219
+ end # module Admin
220
+
221
+ end # module Xolo
data/lib/xolo/admin.rb ADDED
@@ -0,0 +1,139 @@
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
+ # frozen_string_literal: true
8
+
9
+ # Requires
10
+ #########################################
11
+
12
+ # This file is the entry point for loading the Xolo Admin code
13
+ #
14
+ # You can and should require the convenience file 'xolo-admin.rb'
15
+ #
16
+ # require 'xolo-admin'
17
+ #
18
+
19
+ # Standard Libraries
20
+ ######
21
+ require 'openssl'
22
+
23
+ # Monkeypatch OpenSSL::SSL::SSLContext to ignore unexpected EOF errors
24
+ # happens with openssl v3 ??
25
+ # see https://stackoverflow.com/questions/76183622/since-a-ruby-container-upgrade-we-expirience-a-lot-of-opensslsslsslerror
26
+ if OpenSSL::SSL.const_defined?(:OP_IGNORE_UNEXPECTED_EOF)
27
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF
28
+ end
29
+
30
+ require 'openssl'
31
+ require 'faraday'
32
+ require 'faraday/multipart'
33
+ require 'highline'
34
+
35
+ # Monkeypatch OpenSSL::SSL::SSLContext to ignore unexpected EOF errors
36
+ # happens with openssl v3 ??
37
+ # see https://stackoverflow.com/questions/76183622/since-a-ruby-container-upgrade-we-expirience-a-lot-of-opensslsslsslerror
38
+ if OpenSSL::SSL.const_defined?(:OP_IGNORE_UNEXPECTED_EOF)
39
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF
40
+ end
41
+
42
+ # Yes we're using a OpenStruct for our @opts, even though it's very slow.
43
+ # It isn't so slow that it's a problem for processing a CLI tool.
44
+ # The benefit is being able to use either Hash-style references
45
+ # e.g. opts[key] or method-style when you know the key e.g. opts.title
46
+ require 'ostruct'
47
+ require 'open3'
48
+ require 'singleton'
49
+ require 'yaml'
50
+ require 'shellwords'
51
+ require 'tempfile'
52
+ require 'readline'
53
+ require 'io/console'
54
+
55
+ # Use optimist for CLI option processing
56
+ # https://rubygems.org/gems/optimist
57
+ #
58
+ # This version modified to allow 'insert_blanks' which
59
+ # puts blank lines between each option in the help output.
60
+ # See comments in the required file for details.
61
+ #
62
+ require 'optimist_with_insert_blanks'
63
+
64
+ # Xolo Admin code - order matters here
65
+ # more loaded below
66
+ require 'xolo/core'
67
+ require 'xolo/admin/configuration'
68
+
69
+ module Xolo
70
+
71
+ module Admin
72
+
73
+ # Constants
74
+ ##########################
75
+ ##########################
76
+
77
+ EXECUTABLE_FILENAME = 'xadm'
78
+
79
+ # if a streaming line contains this text, we bail out instead of
80
+ # continuing any processing
81
+ STREAMING_OUTPUT_ERROR = 'ERROR'
82
+
83
+ # Module Methods
84
+ ##########################
85
+ ##########################
86
+
87
+ # when this module is included
88
+ def self.included(includer)
89
+ Xolo.verbose_include includer, self
90
+ end
91
+
92
+ # @return [Xolo::Admin::Configuration] our config, available via the module
93
+ ########################
94
+ def self.config
95
+ Xolo::Admin::Configuration.instance
96
+ end
97
+
98
+ # Instance Methods
99
+ ##########################
100
+ ##########################
101
+
102
+ # @return [String] the usage
103
+ ########################
104
+ def usage
105
+ @usage ||= "#{EXECUTABLE_FILENAME} [global-options] command [target] [command-options]"
106
+ end
107
+
108
+ # @return [Xolo::Admin::Configuration] our config available via the admin app instance
109
+ ########################
110
+ def config
111
+ Xolo::Admin::Configuration.instance
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+
118
+ # the rest of the Xolo Admin code - order matters here
119
+ require 'xolo/admin/credentials'
120
+
121
+ require 'xolo/admin/title'
122
+ require 'xolo/admin/version'
123
+
124
+ require 'xolo/admin/options'
125
+ require 'xolo/admin/interactive'
126
+ require 'xolo/admin/command_line'
127
+ require 'xolo/admin/processing'
128
+ require 'xolo/admin/progress_history'
129
+ require 'xolo/admin/validate'
130
+
131
+ require 'xolo/admin/connection'
132
+ require 'xolo/admin/cookie_jar'
133
+ require 'xolo/admin/jamf_pro'
134
+ require 'xolo/admin/title_editor'
135
+
136
+ # A small monkeypatch that allows Readline completion
137
+ # of Highline.ask to optionally use a prompt and be
138
+ # case insensitive
139
+ require 'xolo/admin/highline_terminal'
data/lib/xolo-admin.rb ADDED
@@ -0,0 +1,8 @@
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
+ # Shortcut xolo-admin => xolo/admin
8
+ require 'xolo/admin'
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xolo-admin
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Lasell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-09-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-multipart
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pixar-ruby-extensions
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.11'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.11'
55
+ - !ruby/object:Gem::Dependency
56
+ name: highline
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.1'
69
+ description: |
70
+ == Xolo
71
+ Xolo (sorta pronounced 'show-low') is an HTTPS server and set of command-line tools for macOS that provide automatable access to the software deployment and patch management aspects of {Jamf Pro}[https://www.jamf.com/products/jamf-pro/] and the {Jamf Title Editor}[https://learn.jamf.com/en-US/bundle/title-editor/page/About_Title_Editor.html]. It enhances Jamf Pro's abilities in many ways:
72
+
73
+ - Management of titles and versions/patches is scriptable and automatable, allowing developers and admins to integrate with CI/CD workflows.
74
+ - Simplifies and standardizes the complex, multistep manual process of managing titles and patches using the Title Editor and Patch Management web interfaces.
75
+ - Client installs can be performed by remotely via ssh and/or MDM
76
+ - Automated pre-release piloting of new versions/patches
77
+ - Titles can be expired (auto-uninstalled) after a period of disuse, reclaiming unused licenses.
78
+ - And more!
79
+
80
+ "Xolo" is the short name for the Mexican hairless dog breed {'xoloitzcuintle'}[https://en.wikipedia.org/wiki/Xoloitzcuintle] (show-low-itz-kwint-leh), as personified by Dante in the 2017 Pixar film _Coco_.
81
+
82
+ The xolo-admin gem packages the code needed to run 'xadm', the command-line tool for system administrators to deploy and maintain software titles using Xolo.
83
+ email: xolo@pixar.com
84
+ executables:
85
+ - xadm
86
+ extensions: []
87
+ extra_rdoc_files:
88
+ - README.md
89
+ - LICENSE.txt
90
+ files:
91
+ - LICENSE.txt
92
+ - README.md
93
+ - bin/xadm
94
+ - lib/xolo-admin.rb
95
+ - lib/xolo/admin.rb
96
+ - lib/xolo/admin/command_line.rb
97
+ - lib/xolo/admin/configuration.rb
98
+ - lib/xolo/admin/connection.rb
99
+ - lib/xolo/admin/cookie_jar.rb
100
+ - lib/xolo/admin/credentials.rb
101
+ - lib/xolo/admin/highline_terminal.rb
102
+ - lib/xolo/admin/interactive.rb
103
+ - lib/xolo/admin/jamf_pro.rb
104
+ - lib/xolo/admin/options.rb
105
+ - lib/xolo/admin/processing.rb
106
+ - lib/xolo/admin/progress_history.rb
107
+ - lib/xolo/admin/title.rb
108
+ - lib/xolo/admin/title_editor.rb
109
+ - lib/xolo/admin/validate.rb
110
+ - lib/xolo/admin/version.rb
111
+ homepage: https://pixaranimationstudios.github.io/xolo-home/
112
+ licenses:
113
+ - LicenseRef-LICENSE.txt
114
+ metadata: {}
115
+ post_install_message:
116
+ rdoc_options:
117
+ - "--title"
118
+ - Xolo-Admin
119
+ - "--line-numbers"
120
+ - "--main"
121
+ - README.md
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: 2.6.3
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 3.3.11
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Automation and Standardization for Jamf Pro Patch Management
139
+ test_files: []