xolo-server 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 +7 -0
- data/bin/xoloserver +106 -0
- data/data/com.pixar.xoloserver.plist +29 -0
- data/data/uninstall-pkgs-by-id.zsh +103 -0
- data/lib/xolo/server/app.rb +133 -0
- data/lib/xolo/server/command_line.rb +216 -0
- data/lib/xolo/server/configuration.rb +739 -0
- data/lib/xolo/server/constants.rb +70 -0
- data/lib/xolo/server/helpers/auth.rb +257 -0
- data/lib/xolo/server/helpers/client_data.rb +415 -0
- data/lib/xolo/server/helpers/file_transfers.rb +265 -0
- data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
- data/lib/xolo/server/helpers/log.rb +97 -0
- data/lib/xolo/server/helpers/maintenance.rb +401 -0
- data/lib/xolo/server/helpers/notification.rb +145 -0
- data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
- data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
- data/lib/xolo/server/helpers/title_editor.rb +92 -0
- data/lib/xolo/server/helpers/titles.rb +145 -0
- data/lib/xolo/server/helpers/versions.rb +160 -0
- data/lib/xolo/server/log.rb +286 -0
- data/lib/xolo/server/mixins/changelog.rb +315 -0
- data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
- data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
- data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
- data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
- data/lib/xolo/server/object_locks.rb +102 -0
- data/lib/xolo/server/routes/auth.rb +89 -0
- data/lib/xolo/server/routes/jamf_pro.rb +89 -0
- data/lib/xolo/server/routes/maint.rb +174 -0
- data/lib/xolo/server/routes/title_editor.rb +71 -0
- data/lib/xolo/server/routes/titles.rb +285 -0
- data/lib/xolo/server/routes/uploads.rb +93 -0
- data/lib/xolo/server/routes/versions.rb +261 -0
- data/lib/xolo/server/routes.rb +168 -0
- data/lib/xolo/server/title.rb +1143 -0
- data/lib/xolo/server/version.rb +902 -0
- data/lib/xolo/server.rb +205 -0
- data/lib/xolo-server.rb +8 -0
- metadata +243 -0
|
@@ -0,0 +1,156 @@
|
|
|
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 Server
|
|
14
|
+
|
|
15
|
+
module Helpers
|
|
16
|
+
|
|
17
|
+
# constants and methods for accessing the Jamf Pro server
|
|
18
|
+
# from the Xolo server
|
|
19
|
+
#
|
|
20
|
+
# This is used both as a 'helper' in the Sinatra server,
|
|
21
|
+
# and an included mixin for the Xolo::Server::Title and
|
|
22
|
+
# Xolo::Server::Version classes.
|
|
23
|
+
#
|
|
24
|
+
# This means methods here are available in instances of
|
|
25
|
+
# those classes, and in all routes, views, and helpers in
|
|
26
|
+
# Sinatra.
|
|
27
|
+
#
|
|
28
|
+
module JamfPro
|
|
29
|
+
|
|
30
|
+
# Constants
|
|
31
|
+
#
|
|
32
|
+
##############################
|
|
33
|
+
##############################
|
|
34
|
+
|
|
35
|
+
PATCH_REPORT_UNKNOWN_VERSION = 'UNKNOWN_VERSION'
|
|
36
|
+
PATCH_REPORT_JPAPI_PAGE_SIZE = 500
|
|
37
|
+
|
|
38
|
+
# Module methods
|
|
39
|
+
#
|
|
40
|
+
# These are available as module methods but not as 'helper'
|
|
41
|
+
# methods in sinatra routes & views.
|
|
42
|
+
#
|
|
43
|
+
##############################
|
|
44
|
+
##############################
|
|
45
|
+
|
|
46
|
+
# when this module is included
|
|
47
|
+
##############################
|
|
48
|
+
def self.included(includer)
|
|
49
|
+
Xolo.verbose_include includer, self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# when this module is extended
|
|
53
|
+
def self.extended(extender)
|
|
54
|
+
Xolo.verbose_extend extender, self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Instance methods
|
|
58
|
+
#
|
|
59
|
+
# These are available directly in sinatra routes and views
|
|
60
|
+
#
|
|
61
|
+
##############################
|
|
62
|
+
##############################
|
|
63
|
+
|
|
64
|
+
# @return [String] The start of the Jamf Pro URL for GUI/WebApp access
|
|
65
|
+
################
|
|
66
|
+
def jamf_gui_url
|
|
67
|
+
return @jamf_gui_url if @jamf_gui_url
|
|
68
|
+
|
|
69
|
+
host = Xolo::Server.config.jamf_gui_hostname
|
|
70
|
+
host ||= Xolo::Server.config.jamf_hostname
|
|
71
|
+
port = Xolo::Server.config.jamf_gui_port
|
|
72
|
+
port ||= Xolo::Server.config.jamf_port
|
|
73
|
+
|
|
74
|
+
@jamf_gui_url = "https://#{host}:#{port}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A connection to Jamf Pro via ruby-jss.
|
|
78
|
+
#
|
|
79
|
+
# We don't use the default connection but
|
|
80
|
+
# use this method to create standalone ones as needed
|
|
81
|
+
# and ensure they are disconnected, (or will timeout)
|
|
82
|
+
# when we are done.
|
|
83
|
+
#
|
|
84
|
+
# TODO: allow using APIClients
|
|
85
|
+
#
|
|
86
|
+
# @return [Jamf::Connection] A connection object
|
|
87
|
+
##########################
|
|
88
|
+
def jamf_cnx(refresh: false)
|
|
89
|
+
if refresh
|
|
90
|
+
@jamf_cnx = nil
|
|
91
|
+
log_debug 'Jamf: Refreshing Jamf connection'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return @jamf_cnx if @jamf_cnx
|
|
95
|
+
|
|
96
|
+
@jamf_cnx = Jamf::Connection.new(
|
|
97
|
+
name: "jamf-pro-cnx-#{Time.now.strftime('%F-%T')}",
|
|
98
|
+
host: Xolo::Server.config.jamf_hostname,
|
|
99
|
+
port: Xolo::Server.config.jamf_port,
|
|
100
|
+
verify_cert: Xolo::Server.config.jamf_verify_cert,
|
|
101
|
+
ssl_version: Xolo::Server.config.jamf_ssl_version,
|
|
102
|
+
open_timeout: Xolo::Server.config.jamf_open_timeout,
|
|
103
|
+
timeout: Xolo::Server.config.jamf_timeout,
|
|
104
|
+
user: Xolo::Server.config.jamf_api_user,
|
|
105
|
+
pw: Xolo::Server.config.jamf_api_pw,
|
|
106
|
+
keep_alive: false
|
|
107
|
+
)
|
|
108
|
+
log_debug "Jamf: Connected to Jamf Pro at #{@jamf_cnx.base_url} as user '#{Xolo::Server.config.jamf_api_user}'. KeepAlive: #{@jamf_cnx.keep_alive?}, Expires: #{@jamf_cnx.token.expires}. cnx ID: #{@jamf_cnx.object_id}"
|
|
109
|
+
|
|
110
|
+
@jamf_cnx
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The id of the 'xolo' category in Jamf Pro.s
|
|
114
|
+
#
|
|
115
|
+
def jamf_xolo_category_id
|
|
116
|
+
@jamf_xolo_category_id ||=
|
|
117
|
+
if Jamf::Category.all_names(cnx: jamf_cnx).include? Xolo::Server::JAMF_XOLO_CATEGORY
|
|
118
|
+
Jamf::Category.valid_id(Xolo::Server::JAMF_XOLO_CATEGORY, cnx: jamf_cnx).to_s
|
|
119
|
+
else
|
|
120
|
+
Jamf::Category.create(name: Xolo::Server::JAMF_XOLO_CATEGORY, cnx: jamf_cnx).save
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# if there's a forced_exclusion group defined in the server config
|
|
125
|
+
# return it's name, but only if it exists in jamf. If it doesn't
|
|
126
|
+
# return nil and alert someone
|
|
127
|
+
#
|
|
128
|
+
# @return [String] The valid name of the forced exclusion group
|
|
129
|
+
#####################
|
|
130
|
+
def valid_forced_exclusion_group_name
|
|
131
|
+
return @valid_forced_exclusion_group_name if defined?(@valid_forced_exclusion_group_name)
|
|
132
|
+
|
|
133
|
+
the_grp_name = Xolo::Server.config.forced_exclusion
|
|
134
|
+
|
|
135
|
+
if the_grp_name
|
|
136
|
+
if Jamf::ComputerGroup.all_names(cnx: jamf_cnx).include? the_grp_name
|
|
137
|
+
@valid_forced_exclusion_group_name = the_grp_name
|
|
138
|
+
else
|
|
139
|
+
msg = "ERROR: The forced_exclusion group '#{Xolo::Server.config.forced_exclusion}' in xolo server config does not exist in Jamf"
|
|
140
|
+
log_error msg, alert: true
|
|
141
|
+
@valid_forced_exclusion_group_name = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# not in config
|
|
145
|
+
else
|
|
146
|
+
@valid_forced_exclusion_group_name = nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
end # JamfPro
|
|
151
|
+
|
|
152
|
+
end # Helpers
|
|
153
|
+
|
|
154
|
+
end # Server
|
|
155
|
+
|
|
156
|
+
end # module Xolo
|
|
@@ -0,0 +1,97 @@
|
|
|
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 Server
|
|
14
|
+
|
|
15
|
+
module Helpers
|
|
16
|
+
|
|
17
|
+
# This is mixed in to Xolo::Server::App (as a helper, available in route processing)
|
|
18
|
+
# and in Xolo::Server::Title and Xolo::Server::Version,
|
|
19
|
+
# for simplified access to the main server logger, with access to session IDs
|
|
20
|
+
#
|
|
21
|
+
# The Title and Version objects must be instantiated with the current session object
|
|
22
|
+
# in order for this to work.
|
|
23
|
+
#
|
|
24
|
+
# See Xolo::Server::Helpers::Titles#instantiate_title for how this happens
|
|
25
|
+
#
|
|
26
|
+
# All those things need have have #session set before calling the log_* methods
|
|
27
|
+
module Log
|
|
28
|
+
|
|
29
|
+
# Module Methods
|
|
30
|
+
#######################
|
|
31
|
+
#######################
|
|
32
|
+
|
|
33
|
+
# when this module is included
|
|
34
|
+
def self.included(includer)
|
|
35
|
+
Xolo.verbose_include includer, self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Instance Methods
|
|
39
|
+
#######################
|
|
40
|
+
######################
|
|
41
|
+
|
|
42
|
+
###############################
|
|
43
|
+
def logger
|
|
44
|
+
Xolo::Server.logger
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
###############################
|
|
48
|
+
def session_svr_obj_id
|
|
49
|
+
return @session_svr_obj_id if @session_svr_obj_id
|
|
50
|
+
|
|
51
|
+
@session_svr_obj_id =
|
|
52
|
+
("#{session[:xolo_id]}-#{object_id}" if session[:xolo_id])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
###############################
|
|
56
|
+
def log_debug(msg, alert: false)
|
|
57
|
+
logger.debug(session_svr_obj_id) { msg }
|
|
58
|
+
send_alert msg, :DEBUG if alert
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
###############################
|
|
62
|
+
def log_info(msg, alert: false)
|
|
63
|
+
logger.info(session_svr_obj_id) { msg }
|
|
64
|
+
send_alert msg, :INFO if alert
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
###############################
|
|
68
|
+
def log_warn(msg, alert: false)
|
|
69
|
+
logger.warn(session_svr_obj_id) { msg }
|
|
70
|
+
send_alert msg, :WARNING if alert
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
###############################
|
|
74
|
+
def log_error(msg, alert: false)
|
|
75
|
+
logger.error(session_svr_obj_id) { msg }
|
|
76
|
+
send_alert msg, :ERROR if alert
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
###############################
|
|
80
|
+
def log_fatal(msg, alert: false)
|
|
81
|
+
logger.fatal(session_svr_obj_id) { msg }
|
|
82
|
+
send_alert msg, :FATAL if alert
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
###############################
|
|
86
|
+
def log_unknown(msg, alert: false)
|
|
87
|
+
logger.unknown(session_svr_obj_id) { msg }
|
|
88
|
+
send_alert msg, :UNKNOWN if alert
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end # Log
|
|
92
|
+
|
|
93
|
+
end # Helpers
|
|
94
|
+
|
|
95
|
+
end # Server
|
|
96
|
+
|
|
97
|
+
end # module Xolo
|
|
@@ -0,0 +1,401 @@
|
|
|
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
|
+
module Xolo
|
|
10
|
+
|
|
11
|
+
module Server
|
|
12
|
+
|
|
13
|
+
module Helpers
|
|
14
|
+
|
|
15
|
+
# Nightly cleanup of deprecated and skipped packages.
|
|
16
|
+
#
|
|
17
|
+
# Also, alerts will be posted, and Emails will be sent to the
|
|
18
|
+
# admins who added versions that have been in pilot for more than
|
|
19
|
+
# some period of time.
|
|
20
|
+
#
|
|
21
|
+
#
|
|
22
|
+
module Maintenance
|
|
23
|
+
|
|
24
|
+
# when this module is included
|
|
25
|
+
def self.included(includer)
|
|
26
|
+
Xolo.verbose_include includer, self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Constants
|
|
30
|
+
#####################################
|
|
31
|
+
|
|
32
|
+
# At what hour should the nightly cleanup run?
|
|
33
|
+
CLEANUP_HOUR = 2
|
|
34
|
+
|
|
35
|
+
# on which day of the month should we send the unreleased pilot notifications?
|
|
36
|
+
UNRELEASED_PILOTS_NOTIFICATION_DAY = 1
|
|
37
|
+
|
|
38
|
+
# Once a version becomes deprecated, it will
|
|
39
|
+
# be automatically deleted this many days later.
|
|
40
|
+
# If not set in the server config, this is
|
|
41
|
+
# the default value.
|
|
42
|
+
# use 0 or less to disable cleanup of deprecated versions
|
|
43
|
+
DFT_DEPRECATED_LIFETIME_DAYS = 30
|
|
44
|
+
|
|
45
|
+
# If a pilot has not been released in this many
|
|
46
|
+
# days, notify someone about it weekly, asking
|
|
47
|
+
# to release it or delete it.
|
|
48
|
+
# If not set in the server config, this is the
|
|
49
|
+
# default value.
|
|
50
|
+
DFT_UNRELEASED_PILOTS_NOTIFICATION_DAYS = 180
|
|
51
|
+
|
|
52
|
+
# when doing a full shutdown, we need to unload the launchd plist
|
|
53
|
+
SERVER_LAUNCHD_PLIST = Pathname.new '/Library/LaunchDaemons/com.pixar.xoloserver.plist'
|
|
54
|
+
|
|
55
|
+
# Module Methods
|
|
56
|
+
#####################################
|
|
57
|
+
|
|
58
|
+
# A mutex for the cleanup process
|
|
59
|
+
#
|
|
60
|
+
# TODO: use Concrrent Ruby instead of Mutex
|
|
61
|
+
#
|
|
62
|
+
# @return [Mutex] the mutex
|
|
63
|
+
#####################
|
|
64
|
+
def self.cleanup_mutex
|
|
65
|
+
@cleanup_mutex ||= Mutex.new
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# nightly cleanup is done by a Concurrent::TimerTask, which checks every
|
|
69
|
+
# hour to see if it should do anything.
|
|
70
|
+
#
|
|
71
|
+
# It will only do the cleanup if the current time is in the 2am hour
|
|
72
|
+
# (02:00 - 02:59)
|
|
73
|
+
#
|
|
74
|
+
# We trigger the cleanup by POSTing to /cleanup, so that it runs
|
|
75
|
+
# in the context of a request, having access to Title and Version instantiation.
|
|
76
|
+
#
|
|
77
|
+
# @return [Concurrent::TimerTask] the timed task to do log rotation
|
|
78
|
+
######################################
|
|
79
|
+
def self.cleanup_timer_task
|
|
80
|
+
return @cleanup_timer_task if @cleanup_timer_task
|
|
81
|
+
|
|
82
|
+
@cleanup_timer_task =
|
|
83
|
+
Concurrent::TimerTask.new(execution_interval: 3600) { post_to_start_cleanup }
|
|
84
|
+
|
|
85
|
+
Xolo::Server.logger.info 'Created Concurrent::TimerTask for nightly cleanup.'
|
|
86
|
+
@cleanup_timer_task
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# When was our last cleanup?
|
|
90
|
+
# @return [Time] the time of the last cleanup, or the epoch if never
|
|
91
|
+
######################################
|
|
92
|
+
def self.last_cleanup
|
|
93
|
+
@last_cleanup ||= Time.at(0)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Set the time of the last cleanup
|
|
97
|
+
# @param time [Time] the time of the last cleanup
|
|
98
|
+
# @return [Time] the time of the last cleanup
|
|
99
|
+
######################################
|
|
100
|
+
def self.last_cleanup=(time)
|
|
101
|
+
@last_cleanup = time
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# post to the server to start the cleanup process
|
|
105
|
+
# This is done so that the cleanup can run in the context of a request,
|
|
106
|
+
# having access to Title and Version instantiation.
|
|
107
|
+
#
|
|
108
|
+
# @param force [Boolean] force the cleanup to run now
|
|
109
|
+
# @return [void]
|
|
110
|
+
######################################
|
|
111
|
+
def self.post_to_start_cleanup(force: false)
|
|
112
|
+
if Xolo::Server.shutting_down?
|
|
113
|
+
Xolo::Server.logger.info 'Not starting cleanup, server is shutting down'
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# only run the cleanup if it's between 2am and 3am
|
|
118
|
+
# and the last one was more than 23 hrs ago
|
|
119
|
+
last_cleanup_hrs_ago = (Time.now - last_cleanup) / 3600
|
|
120
|
+
return unless force || (Time.now.hour == CLEANUP_HOUR && last_cleanup_hrs_ago > 23)
|
|
121
|
+
|
|
122
|
+
uri = URI.parse "https://#{Xolo::Server::Helpers::Auth::IPV4_LOOPBACK}/maint/cleanup-internal"
|
|
123
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
|
124
|
+
https.use_ssl = true
|
|
125
|
+
# The server cert may be self-signed and/or doesn't
|
|
126
|
+
# match the hostname, so we need to disable verification
|
|
127
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
128
|
+
|
|
129
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
130
|
+
request['Authorization'] = Xolo::Server::Helpers::Auth.internal_auth_token_header
|
|
131
|
+
|
|
132
|
+
response = https.request(request)
|
|
133
|
+
Xolo::Server.logger.info "Cleanup request response: #{response.code} #{response.body}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Cleanup things that need to be cleaned up
|
|
137
|
+
# @return [void]
|
|
138
|
+
################################
|
|
139
|
+
def run_cleanup
|
|
140
|
+
if Xolo::Server.shutting_down?
|
|
141
|
+
log_info 'Cleanup: Not starting cleanup, server is shutting down'
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
# TODO: Use Concurrent ruby rather than this instance variable
|
|
145
|
+
mutex = Xolo::Server::Helpers::Maintenance.cleanup_mutex
|
|
146
|
+
|
|
147
|
+
if mutex.locked?
|
|
148
|
+
log_warn 'Cleanup: already running, skipping this run'
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
mutex.lock
|
|
152
|
+
log_info 'Cleanup: starting'
|
|
153
|
+
|
|
154
|
+
# add new cleanup tasks/methods here
|
|
155
|
+
accept_title_editor_eas
|
|
156
|
+
cleanup_versions
|
|
157
|
+
|
|
158
|
+
log_info 'Cleanup: complete'
|
|
159
|
+
ensure
|
|
160
|
+
mutex&.unlock
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# look for any titles that need their Title Editor EA's accepted,
|
|
164
|
+
# and auto accept them if we need to
|
|
165
|
+
# @return [void]
|
|
166
|
+
######################################
|
|
167
|
+
def accept_title_editor_eas
|
|
168
|
+
unless Xolo::Server.config.jamf_auto_accept_xolo_eas
|
|
169
|
+
log_info 'Cleanup: The xolo server is not configured to auto-accept Title Editor EAs'
|
|
170
|
+
return
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
log_info 'Cleanup: Looking for Title Editor EAs to auto-accept'
|
|
174
|
+
|
|
175
|
+
# TODO: Be DRY with this stuff and similar in title_jamf_access.rb
|
|
176
|
+
Xolo::Server::Title.all_titles.each do |title|
|
|
177
|
+
title_obj = instantiate_title title
|
|
178
|
+
next unless title_obj.jamf_patch_ea_awaiting_acceptance?
|
|
179
|
+
|
|
180
|
+
log_info "Cleanup: Auto-accepting Title Editor EA for title '#{title}'"
|
|
181
|
+
title_obj.accept_jamf_patch_ea_via_api
|
|
182
|
+
rescue => e
|
|
183
|
+
log_error "Cleanup: Error auto-accepting Title Editor EA for title '#{title}': #{e}"
|
|
184
|
+
end # Xolo::Server::Title.all_titles.each
|
|
185
|
+
|
|
186
|
+
log_info 'Cleanup: Done with Title Editor EAs to auto-accept'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Cleanup versions.
|
|
190
|
+
# @return [void]
|
|
191
|
+
################################
|
|
192
|
+
def cleanup_versions
|
|
193
|
+
log_info 'Cleanup: cleaning up deprecated and skipped versions'
|
|
194
|
+
|
|
195
|
+
Xolo::Server::Title.all_titles.each do |title|
|
|
196
|
+
title_obj = instantiate_title title
|
|
197
|
+
|
|
198
|
+
title_obj.version_objects.each do |version|
|
|
199
|
+
if version.deprecated?
|
|
200
|
+
cleanup_deprecated_version version
|
|
201
|
+
elsif version.skipped?
|
|
202
|
+
cleanup_skipped_version version
|
|
203
|
+
end # case
|
|
204
|
+
end # each version
|
|
205
|
+
|
|
206
|
+
notify_admins_of_unreleased_pilots(title_obj)
|
|
207
|
+
end # each title
|
|
208
|
+
|
|
209
|
+
Xolo::Server::Helpers::Maintenance.last_cleanup = Time.now
|
|
210
|
+
log_info 'Cleanup: versions cleanup complete'
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Cleanup a deprecated version.
|
|
214
|
+
# @param version [Xolo::Server::Version] the version to cleanup
|
|
215
|
+
# @return [void]
|
|
216
|
+
################################
|
|
217
|
+
def cleanup_deprecated_version(version)
|
|
218
|
+
# do nothing if the deprecated_lifetime_days is 0 or less
|
|
219
|
+
return unless deprecated_lifetime_days.positive?
|
|
220
|
+
|
|
221
|
+
# how many days has this version been deprecated?
|
|
222
|
+
days_deprecated = (Time.now - version.deprecation_date) / 86_400
|
|
223
|
+
return unless days_deprecated > deprecated_lifetime_days
|
|
224
|
+
|
|
225
|
+
log_info "Cleanup: Deleting deprecated version '#{version.version}' of title '#{version.title}'"
|
|
226
|
+
version.delete
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Cleanup a skipped version.
|
|
230
|
+
# @param version [Xolo::Server::Version] the version to cleanup
|
|
231
|
+
# @return [void]
|
|
232
|
+
################################
|
|
233
|
+
def cleanup_skipped_version(version)
|
|
234
|
+
return if Xolo::Server.config.keep_skipped_versions
|
|
235
|
+
|
|
236
|
+
log_info "Cleanup: Deleting skipped version '#{version.version}' of title '#{version.title}'"
|
|
237
|
+
version.delete
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Notify the admins about unreleased pilots if needed
|
|
241
|
+
# @return [void]
|
|
242
|
+
################################
|
|
243
|
+
def notify_admins_of_unreleased_pilots(title_obj)
|
|
244
|
+
return unless Time.now.day == UNRELEASED_PILOTS_NOTIFICATION_DAY
|
|
245
|
+
return unless unreleased_pilots_notification_days.positive?
|
|
246
|
+
return unless title_obj.latest_version
|
|
247
|
+
|
|
248
|
+
latest_vers_obj = instantiate_version title: title_obj, version: title_obj.latest_version
|
|
249
|
+
return unless latest_vers_obj.pilot?
|
|
250
|
+
|
|
251
|
+
days_in_pilot = ((Time.now - latest_vers_obj.creation_date) / 86_400).to_i
|
|
252
|
+
|
|
253
|
+
return unless days_in_pilot > unreleased_pilots_notification_days
|
|
254
|
+
|
|
255
|
+
alert_msg = "Cleanup: Notifying #{title_obj.contact_email} about unreleased pilot '#{latest_vers}' of title '#{title_obj.title}', in pilot for #{days_in_pilot} days"
|
|
256
|
+
|
|
257
|
+
log_info alert_msg
|
|
258
|
+
send_alert alert_msg
|
|
259
|
+
|
|
260
|
+
email_msg = <<~MSG
|
|
261
|
+
The newest version '#{latest_vers_obj.version}' of title '#{title_obj.title}' has been in pilot for #{days_in_pilot} days, which makes it seem like it's not going to be released.
|
|
262
|
+
|
|
263
|
+
To reduce clutter, please consider releasing it, deleting it, or deleting the whole title if it's no longer needed.
|
|
264
|
+
|
|
265
|
+
If this is intentional, you can ignore this monthly message.
|
|
266
|
+
MSG
|
|
267
|
+
send_email to: title_obj.contact_email, subject: 'Unreleased Pilot Notification', msg: email_msg
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# how many days can a version be deprecated?
|
|
271
|
+
# @return [Integer] the number of days a version can be deprecated
|
|
272
|
+
################################
|
|
273
|
+
def deprecated_lifetime_days
|
|
274
|
+
@deprecated_lifetime_days ||= Xolo::Server.config.deprecated_lifetime_days || DFT_DEPRECATED_LIFETIME_DAYS
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Notify the admins about unreleased pilots when the newest one is older than
|
|
278
|
+
# this many days.
|
|
279
|
+
def unreleased_pilots_notification_days
|
|
280
|
+
@unreleased_pilots_notification_days ||=
|
|
281
|
+
Xolo::Server.config.unreleased_pilots_notification_days || DFT_UNRELEASED_PILOTS_NOTIFICATION_DAYS
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Shutdown the server
|
|
285
|
+
# @return [void]
|
|
286
|
+
################################
|
|
287
|
+
def shutdown_server(restart)
|
|
288
|
+
# let all the routes know we are shutting down
|
|
289
|
+
Xolo::Server.shutting_down = true
|
|
290
|
+
|
|
291
|
+
progress "Server Shutdown by #{session[:admin]}", log: :info
|
|
292
|
+
|
|
293
|
+
stop_cleanup_timer_task
|
|
294
|
+
stop_log_rotation_timer_task
|
|
295
|
+
shutdown_pkg_deletion_pool
|
|
296
|
+
wait_for_object_locks
|
|
297
|
+
wait_for_progress_streams
|
|
298
|
+
|
|
299
|
+
# without unloading the launchd job, the server will restart automatically
|
|
300
|
+
# when we tell it to quit
|
|
301
|
+
if restart
|
|
302
|
+
progress 'Restarting the server now', log: :info
|
|
303
|
+
Xolo::Server::App.quit!
|
|
304
|
+
else
|
|
305
|
+
progress 'Shutting down the server now', log: :info
|
|
306
|
+
unload_server_launchd
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# full shutdown of the server by unloading the launchd plist
|
|
311
|
+
# @return [void]
|
|
312
|
+
################################
|
|
313
|
+
def unload_server_launchd
|
|
314
|
+
log_info 'Unloading the server launchd plist'
|
|
315
|
+
system "/bin/launchctl unload #{SERVER_LAUNCHD_PLIST}"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Stop the cleanup timer task
|
|
319
|
+
# @return [void]
|
|
320
|
+
################################
|
|
321
|
+
def stop_cleanup_timer_task
|
|
322
|
+
progress 'Stopping the cleanup timer task', log: :info
|
|
323
|
+
Xolo::Server::Helpers::Maintenance.cleanup_timer_task.shutdown
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Stop the log rotation timer task
|
|
327
|
+
# @return [void]
|
|
328
|
+
################################
|
|
329
|
+
def stop_log_rotation_timer_task
|
|
330
|
+
progress 'Stopping the log rotation timer task', log: :info
|
|
331
|
+
Xolo::Server::Log.log_rotation_timer_task.shutdown
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Wait for all object locks to be released
|
|
335
|
+
# @return [void]
|
|
336
|
+
################################
|
|
337
|
+
def wait_for_object_locks
|
|
338
|
+
Xolo::Server.remove_expired_object_locks
|
|
339
|
+
|
|
340
|
+
until Xolo::Server.object_locks.empty?
|
|
341
|
+
progress 'Waiting for object locks to be released', log: :info
|
|
342
|
+
log_debug "Object locks: #{Xolo::Server.object_locks.inspect}"
|
|
343
|
+
sleep 5
|
|
344
|
+
Xolo::Server.remove_expired_object_locks
|
|
345
|
+
end
|
|
346
|
+
progress 'All object locks released', log: :info
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Wait for all progress streams to finish
|
|
350
|
+
# @return [void]
|
|
351
|
+
################################
|
|
352
|
+
def wait_for_progress_streams
|
|
353
|
+
prefix = Xolo::Server::Helpers::ProgressStreaming::PROGRESS_THREAD_NAME_PREFIX
|
|
354
|
+
prog_threads = Thread.list.select { |th| th.name.to_s.start_with? prefix }
|
|
355
|
+
# remove our own thread from the list
|
|
356
|
+
prog_threads.delete Thread.current
|
|
357
|
+
prog_threads.delete @streaming_thread
|
|
358
|
+
|
|
359
|
+
until prog_threads.empty?
|
|
360
|
+
progress 'Waiting for progress streams to finish', log: :info
|
|
361
|
+
log_debug "Progress stream threads: #{prog_threads.map(&:name)}}"
|
|
362
|
+
sleep 5
|
|
363
|
+
prog_threads = Thread.list.select { |th| th.name.to_s.start_with? prefix }
|
|
364
|
+
# remove our own thread from the list
|
|
365
|
+
prog_threads.delete Thread.current
|
|
366
|
+
prog_threads.delete @streaming_thread
|
|
367
|
+
end
|
|
368
|
+
progress 'All progress streams finished', log: :info
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Shutdown the pkg deletion pool
|
|
372
|
+
# @return [void]
|
|
373
|
+
################################
|
|
374
|
+
def shutdown_pkg_deletion_pool
|
|
375
|
+
# Start the shutdown of the pkg_deletion_pool. Will finish anything
|
|
376
|
+
# in the queue, but not accept any new tasks.
|
|
377
|
+
pkg_pool = Xolo::Server::Version.pkg_deletion_pool
|
|
378
|
+
pkg_pool.shutdown
|
|
379
|
+
pkg_pool_shutdown_start = Time.now
|
|
380
|
+
progress 'Shutting down pkg deletion pool', log: :info
|
|
381
|
+
# returns true when shutdown is complete
|
|
382
|
+
until pkg_pool.wait_for_termination(20)
|
|
383
|
+
msg = "..Waiting for pkg deletion pool to finish, processing: #{pkg_pool.length}, in queue: #{pkg_pool.queue_length}"
|
|
384
|
+
progress msg, log: :debug
|
|
385
|
+
next unless Time.now - pkg_pool_shutdown_start > Xolo::Server::Constants::MAX_JAMF_WAIT_FOR_PKG_DELETION
|
|
386
|
+
|
|
387
|
+
msg = 'ERROR: Timeout waiting for pkg deletion pool to finish, some pkgs may not be deleted'
|
|
388
|
+
progress msg, log: :error
|
|
389
|
+
pkg_pool.kill
|
|
390
|
+
break
|
|
391
|
+
end
|
|
392
|
+
progress 'Pkg deletion queue is empty'
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
end # module Maintenance
|
|
396
|
+
|
|
397
|
+
end # module Helpers
|
|
398
|
+
|
|
399
|
+
end # Server
|
|
400
|
+
|
|
401
|
+
end # module
|