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,145 @@
|
|
|
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 working with Xolo Titles on the server
|
|
18
|
+
module Titles
|
|
19
|
+
|
|
20
|
+
# Module Methods
|
|
21
|
+
#######################
|
|
22
|
+
#######################
|
|
23
|
+
|
|
24
|
+
# when this module is included
|
|
25
|
+
def self.included(includer)
|
|
26
|
+
Xolo.verbose_include includer, self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Instance Methods
|
|
30
|
+
#######################
|
|
31
|
+
######################
|
|
32
|
+
|
|
33
|
+
# A list of all known titles
|
|
34
|
+
# @return [Array<String>]
|
|
35
|
+
############
|
|
36
|
+
def all_titles
|
|
37
|
+
Xolo::Server::Title.all_titles
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A an array of all server titles as Title objects
|
|
41
|
+
# @return [Array<Xolo::Server::Title>]
|
|
42
|
+
############
|
|
43
|
+
def all_title_objects
|
|
44
|
+
all_titles.map { |t| instantiate_title t }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Instantiate a Server::Title with access to the Sinatra app instance,
|
|
48
|
+
#
|
|
49
|
+
# If given a string, use it with .load to read the title from disk
|
|
50
|
+
#
|
|
51
|
+
# If given a Hash, use it with .new to instantiate fresh
|
|
52
|
+
#
|
|
53
|
+
# In all cases, set the session, to use for logging
|
|
54
|
+
# (the reason this method exists)
|
|
55
|
+
#
|
|
56
|
+
# @param data [Hash, String] hash to use with .new or name to use with .load
|
|
57
|
+
#
|
|
58
|
+
# @return [Xolo::Server::Title]
|
|
59
|
+
#################
|
|
60
|
+
def instantiate_title(data)
|
|
61
|
+
title =
|
|
62
|
+
case data
|
|
63
|
+
when Hash
|
|
64
|
+
Xolo::Server::Title.new data
|
|
65
|
+
|
|
66
|
+
when String
|
|
67
|
+
halt_on_missing_title data
|
|
68
|
+
Xolo::Server::Title.load data
|
|
69
|
+
|
|
70
|
+
else
|
|
71
|
+
msg = 'Invalid data to instantiate a Xolo::Server::Title'
|
|
72
|
+
log_error msg
|
|
73
|
+
|
|
74
|
+
halt 400, { status: 400, error: msg }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
title.server_app_instance = self
|
|
78
|
+
title
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Halt 404 if a title doesn't exist
|
|
82
|
+
# @pararm [String] The title of a Title
|
|
83
|
+
# @return [void]
|
|
84
|
+
##################
|
|
85
|
+
def halt_on_missing_title(title)
|
|
86
|
+
return if all_titles.include? title
|
|
87
|
+
|
|
88
|
+
msg = "Title '#{title}' does not exist."
|
|
89
|
+
log_debug "ERROR: #{msg}"
|
|
90
|
+
halt 404, { status: 404, error: msg }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Halt 409 if a title already exists
|
|
94
|
+
# @pararm [String] The title of a Title
|
|
95
|
+
# @return [void]
|
|
96
|
+
##################
|
|
97
|
+
def halt_on_existing_title(title)
|
|
98
|
+
return unless all_titles.include? title
|
|
99
|
+
|
|
100
|
+
msg = "Title '#{title}' already exists."
|
|
101
|
+
log_debug "ERROR: #{msg}"
|
|
102
|
+
halt 409, { status: 409, error: msg }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Halt 409 if a title is locked
|
|
106
|
+
# @pararm [String] The title of a Title
|
|
107
|
+
# @return [void]
|
|
108
|
+
##################
|
|
109
|
+
def halt_on_locked_title(title)
|
|
110
|
+
return unless Xolo::Server::Title.locked? title
|
|
111
|
+
|
|
112
|
+
msg = "Title '#{title}' is being modified by another admin. Try again later."
|
|
113
|
+
log_debug "ERROR: #{msg}"
|
|
114
|
+
halt 409, { status: 409, error: msg }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# when freezing or thawing, are we dealing with a list of computers
|
|
118
|
+
# or a list of users, for whom we need to get all their assigned computers
|
|
119
|
+
# @param targets [Array<String>] a list of computers or usernames
|
|
120
|
+
# @param users [Boolean] is the list usernames? if not, its computers
|
|
121
|
+
# @return [Array<String>] a list of computers to freeze or thaw
|
|
122
|
+
########################
|
|
123
|
+
def expand_freeze_thaw_targets(targets:, users:)
|
|
124
|
+
return targets unless users
|
|
125
|
+
|
|
126
|
+
log_debug "Expanding user list to freeze or thaw: #{targets}"
|
|
127
|
+
|
|
128
|
+
expanded_targets = []
|
|
129
|
+
all_users = Jamf::User.all_names(cnx: jamf_cnx)
|
|
130
|
+
targets.each do |user|
|
|
131
|
+
next unless all_users.include? user
|
|
132
|
+
|
|
133
|
+
expanded_targets += Jamf::User.fetch(name: user, cnx: jamf_cnx).computers.map { |c| c[:name] }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
expanded_targets.uniq
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
end # TitleEditor
|
|
140
|
+
|
|
141
|
+
end # Helpers
|
|
142
|
+
|
|
143
|
+
end # Server
|
|
144
|
+
|
|
145
|
+
end # module Xolo
|
|
@@ -0,0 +1,160 @@
|
|
|
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 working with Xolo Versions on the server
|
|
18
|
+
# As a helper, these are available in the App instance context, for all
|
|
19
|
+
# routes and views
|
|
20
|
+
module Versions
|
|
21
|
+
|
|
22
|
+
# Module Methods
|
|
23
|
+
#######################
|
|
24
|
+
#######################
|
|
25
|
+
|
|
26
|
+
# when this module is included
|
|
27
|
+
def self.included(includer)
|
|
28
|
+
Xolo.verbose_include includer, self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Instance Methods
|
|
32
|
+
#######################
|
|
33
|
+
######################
|
|
34
|
+
|
|
35
|
+
# The default minimum OS for versions
|
|
36
|
+
# # @return [String] the default minimum OS for versions
|
|
37
|
+
#############
|
|
38
|
+
def default_min_os
|
|
39
|
+
if Xolo::Server.config.default_min_os.pix_empty?
|
|
40
|
+
Xolo::Core::BaseClasses::Version::DEFAULT_MIN_OS.to_s
|
|
41
|
+
else
|
|
42
|
+
Xolo::Server.config.default_min_os.to_s
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A list of all known versions of a title
|
|
47
|
+
# @return [Array<String>]
|
|
48
|
+
############
|
|
49
|
+
def all_versions(title)
|
|
50
|
+
Xolo::Server::Version.all_versions(title)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A list of all known versions of a title
|
|
54
|
+
# @return [Array<Xolo::Server::Version>]
|
|
55
|
+
############
|
|
56
|
+
def all_version_instances(title)
|
|
57
|
+
all_versions(title).map { |v| instantiate_version title: title, version: v }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Instantiate a Server::Version, with access to the Sinata App instance
|
|
61
|
+
#
|
|
62
|
+
# If given a Hash, use it with .new to instantiate fresh
|
|
63
|
+
#
|
|
64
|
+
# If given a title and version, the title may be a String, the title's
|
|
65
|
+
# title, or a Xolo::Server::Title object. If it's a Xolo::Server::Title
|
|
66
|
+
# that object will be used as the title_object for the version object.
|
|
67
|
+
#
|
|
68
|
+
# In all cases, set the server_app_instance in the new version onject
|
|
69
|
+
# to use for access from the version object to the Sinatra App instance
|
|
70
|
+
# for the session and api connection objects
|
|
71
|
+
#
|
|
72
|
+
# @param data [Hash] hash to use with .new
|
|
73
|
+
# @param title [String, Xolo::Server::Title] title to use with .load
|
|
74
|
+
# @param version [String] version to use with .load
|
|
75
|
+
#
|
|
76
|
+
# @return [Xolo::Server::Version]
|
|
77
|
+
#################
|
|
78
|
+
def instantiate_version(data = nil, title: nil, version: nil)
|
|
79
|
+
title_obj = nil
|
|
80
|
+
|
|
81
|
+
if data
|
|
82
|
+
title = data[:title]
|
|
83
|
+
elsif title.is_a?(Xolo::Server::Title)
|
|
84
|
+
title_obj = title
|
|
85
|
+
title = title_obj.title
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
vers =
|
|
89
|
+
if data.is_a? Hash
|
|
90
|
+
Xolo::Server::Version.new data
|
|
91
|
+
|
|
92
|
+
elsif title && version
|
|
93
|
+
halt_on_missing_title title
|
|
94
|
+
halt_on_missing_version title, version
|
|
95
|
+
|
|
96
|
+
Xolo::Server::Version.load title, version
|
|
97
|
+
else
|
|
98
|
+
msg = 'Invalid data to instantiate a Xolo::Server::Version'
|
|
99
|
+
log_error msg
|
|
100
|
+
halt 400, { status: 400, error: msg }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
vers.title_object = title_obj || instantiate_title(title)
|
|
104
|
+
vers.server_app_instance = self
|
|
105
|
+
vers
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Halt 404 if a version doesn't exist
|
|
109
|
+
# @pararm [String] The title of a Title
|
|
110
|
+
# @return [void]
|
|
111
|
+
##################
|
|
112
|
+
def halt_on_missing_version(title, version)
|
|
113
|
+
return if all_versions(title).include? version
|
|
114
|
+
|
|
115
|
+
msg = "No version '#{version}' for title '#{title}'."
|
|
116
|
+
log_debug "ERROR: #{msg}"
|
|
117
|
+
resp_body = @streaming_now ? msg : { status: 404, error: msg }
|
|
118
|
+
|
|
119
|
+
# don't halt if we're streaming, just error out
|
|
120
|
+
raise Xolo::NoSuchItemError, msg if @streaming_now
|
|
121
|
+
|
|
122
|
+
halt 404, resp_body
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Halt 409 if a title already exists
|
|
126
|
+
# @pararm [String] The title of a Title
|
|
127
|
+
# @return [void]
|
|
128
|
+
##################
|
|
129
|
+
def halt_on_existing_version(title, version)
|
|
130
|
+
return unless all_versions(title).include? version
|
|
131
|
+
|
|
132
|
+
msg = "Version '#{version}' of title '#{title}' already exists."
|
|
133
|
+
log_debug "ERROR: #{msg}"
|
|
134
|
+
resp_body = @streaming_now ? msg : { status: 409, error: msg }
|
|
135
|
+
|
|
136
|
+
# don't halt if we're streaming, just error out
|
|
137
|
+
raise Xolo::NoSuchItemError, msg if @streaming_now
|
|
138
|
+
|
|
139
|
+
halt 409, resp_body
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Halt 409 if a version is locked
|
|
143
|
+
# @pararm [String] The title of a Title
|
|
144
|
+
# @return [void]
|
|
145
|
+
##################
|
|
146
|
+
def halt_on_locked_version(title, version)
|
|
147
|
+
return unless Xolo::Server::Version.locked? title, version
|
|
148
|
+
|
|
149
|
+
msg = "Version '#{version}' of title '#{title}' is being modified by another admin. Try again later."
|
|
150
|
+
log_debug "ERROR: #{msg}"
|
|
151
|
+
halt 409, { status: 409, error: msg }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
end # TitleEditor
|
|
155
|
+
|
|
156
|
+
end # Helpers
|
|
157
|
+
|
|
158
|
+
end # Server
|
|
159
|
+
|
|
160
|
+
end # module Xolo
|
|
@@ -0,0 +1,286 @@
|
|
|
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
|
+
# constants and methods for writing to the log
|
|
16
|
+
module Log
|
|
17
|
+
|
|
18
|
+
# when this module is included
|
|
19
|
+
def self.included(includer)
|
|
20
|
+
Xolo.verbose_include includer, self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
DATETIME_FORMAT = '%F %T'
|
|
24
|
+
|
|
25
|
+
# Easier for reporting level changes - the index is the severity number
|
|
26
|
+
LEVELS = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
|
|
27
|
+
|
|
28
|
+
# THe log format - we use 'progname' to hole the
|
|
29
|
+
# session object, if there is one.
|
|
30
|
+
#
|
|
31
|
+
LOG_FORMATTER = proc do |severity, datetime, progname, msg|
|
|
32
|
+
progname &&= " #{progname}"
|
|
33
|
+
"#{datetime.strftime DATETIME_FORMAT} #{severity}#{progname}: #{msg}\n"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
LOG_DIR = Xolo::Server::Constants::DATA_DIR + 'logs'
|
|
37
|
+
LOG_FILE_NAME = 'xoloserver.log'
|
|
38
|
+
LOG_FILE = LOG_DIR + LOG_FILE_NAME
|
|
39
|
+
|
|
40
|
+
# log rotation, compression, pruning
|
|
41
|
+
|
|
42
|
+
# keep this many days of logs total
|
|
43
|
+
DFT_LOG_DAYS_TO_KEEP = 30
|
|
44
|
+
|
|
45
|
+
# compress the log files when they are this many days old
|
|
46
|
+
DFT_LOG_COMPRESS_AFTER_DAYS = 7
|
|
47
|
+
|
|
48
|
+
# shell out to bzip2 - no need for another gem requirement.
|
|
49
|
+
BZIP2 = '/usr/bin/bzip2'
|
|
50
|
+
|
|
51
|
+
# compressed files have this extension
|
|
52
|
+
BZIPPED_EXTNAME = '.bz2'
|
|
53
|
+
|
|
54
|
+
# This file is touched after each log rotation run.
|
|
55
|
+
# Its mtime is used to decide if we should to it again
|
|
56
|
+
LAST_ROTATION_FILE = LOG_DIR + 'last_log_rotation'
|
|
57
|
+
|
|
58
|
+
# When we rotate the main log to .0 we repoint the logger to this
|
|
59
|
+
# temp filename, then rename the main log to .0, then rename this
|
|
60
|
+
# temp file to the main log. This way the logger never has to be
|
|
61
|
+
# reinitialized.
|
|
62
|
+
TEMP_LOG_FILE = LOG_DIR + 'temp_xoloserver.log'
|
|
63
|
+
|
|
64
|
+
# top-level logger for the server as a whole
|
|
65
|
+
#############################################
|
|
66
|
+
def self.logger
|
|
67
|
+
return @logger if @logger
|
|
68
|
+
|
|
69
|
+
LOG_DIR.mkpath
|
|
70
|
+
LOG_FILE.pix_touch
|
|
71
|
+
|
|
72
|
+
@logger = Logger.new(
|
|
73
|
+
LOG_FILE,
|
|
74
|
+
datetime_format: DATETIME_FORMAT,
|
|
75
|
+
formatter: LOG_FORMATTER
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@logger
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A mutex for the log rotation process
|
|
82
|
+
#
|
|
83
|
+
# TODO: use Concrrent Ruby instead of Mutex
|
|
84
|
+
#
|
|
85
|
+
# @return [Mutex] the mutex
|
|
86
|
+
#####################
|
|
87
|
+
def self.rotation_mutex
|
|
88
|
+
@rotation_mutex ||= Mutex.new
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# change log level of the server logger, new requests should inherit it
|
|
92
|
+
#############################################
|
|
93
|
+
# def self.set_level(level, user: :unknown)
|
|
94
|
+
# lvl_const = level.to_s.upcase.to_sym
|
|
95
|
+
# if Logger.constants.include? lvl_const
|
|
96
|
+
# lvl = Logger.const_get lvl_const
|
|
97
|
+
# logger.debug "changing log level to #{lvl_const} (#{lvl}) by #{user}"
|
|
98
|
+
# logger.level = lvl
|
|
99
|
+
# logger.info "log level changed to #{lvl_const} by #{user}"
|
|
100
|
+
|
|
101
|
+
# { loglevel: lvl_const }
|
|
102
|
+
# else
|
|
103
|
+
# { error: "Unknown level '#{level}', use one of: debug, info, warn, error, fatal, unknown" }
|
|
104
|
+
# end
|
|
105
|
+
# end
|
|
106
|
+
|
|
107
|
+
# Log rotation is done by a Concurrent::TimerTask, which checks every
|
|
108
|
+
# 5 minutes to see if it should do anything.
|
|
109
|
+
# It will only do a rotation if the current time is in the midnight hour
|
|
110
|
+
# (00:00 - 00:59) AND if the last rotation was more than 23 hours ago.
|
|
111
|
+
#
|
|
112
|
+
# @return [Concurrent::TimerTask] the timed task to do log rotation
|
|
113
|
+
def self.log_rotation_timer_task
|
|
114
|
+
return @log_rotation_timer_task if @log_rotation_timer_task
|
|
115
|
+
|
|
116
|
+
@log_rotation_timer_task =
|
|
117
|
+
Concurrent::TimerTask.new(execution_interval: 300) { rotate_logs }
|
|
118
|
+
|
|
119
|
+
logger.info 'Created Concurrent::TimerTask for nightly log rotation.'
|
|
120
|
+
@log_rotation_timer_task
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# rotate the log file, keeping some number of old ones and possibly
|
|
124
|
+
# compressing them after some time.
|
|
125
|
+
# The log rotation built into ruby's Logger class doesn't allow this kind of
|
|
126
|
+
# behavior, And I don't want to require yet another 3rd party gem.
|
|
127
|
+
#
|
|
128
|
+
# @param force [Boolean] force rotation even if not midnight
|
|
129
|
+
#
|
|
130
|
+
# @return [void]
|
|
131
|
+
###############################
|
|
132
|
+
def self.rotate_logs(force: false)
|
|
133
|
+
return unless rotate_logs_now?(force: force)
|
|
134
|
+
|
|
135
|
+
# TODO: Use Concurrent ruby rather than this instance variable
|
|
136
|
+
mutex = Xolo::Server::Log.rotation_mutex
|
|
137
|
+
|
|
138
|
+
if mutex.locked?
|
|
139
|
+
log_warn 'Log rotation already running, skipping this run'
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
mutex.lock
|
|
144
|
+
|
|
145
|
+
logger.info 'Starting Log Rotation'
|
|
146
|
+
|
|
147
|
+
# how many to keep?
|
|
148
|
+
days_to_keep = Xolo::Server.config.log_days_to_keep || Xolo::Server::Log::DFT_LOG_DAYS_TO_KEEP
|
|
149
|
+
|
|
150
|
+
# when to compress?
|
|
151
|
+
compress_after = Xolo::Server.config.log_compress_after_days || Xolo::Server::Log::DFT_LOG_COMPRESS_AFTER_DAYS
|
|
152
|
+
|
|
153
|
+
# no compression if compress_after is less than zero or greater/equal to days to keeps
|
|
154
|
+
compress_after = nil if compress_after.negative? || compress_after >= days_to_keep
|
|
155
|
+
|
|
156
|
+
# work down thru the number to keep
|
|
157
|
+
days_to_keep.downto(1) do |age|
|
|
158
|
+
# the previous file to the newly aged file
|
|
159
|
+
# if we're moving to a newly aged .3, this is .2
|
|
160
|
+
prev_age = age - 1
|
|
161
|
+
|
|
162
|
+
# rename a previous file to n+1, e.g. file.2 becomes file.3
|
|
163
|
+
# This will overwrite an existing new_file, which is how we
|
|
164
|
+
# delete the oldest
|
|
165
|
+
prev_file = LOG_DIR + "#{LOG_FILE_NAME}.#{prev_age}"
|
|
166
|
+
new_file = LOG_DIR + "#{LOG_FILE_NAME}.#{age}"
|
|
167
|
+
if prev_file.file?
|
|
168
|
+
logger.info "Moving log file #{prev_file.basename} => #{new_file.basename}"
|
|
169
|
+
prev_file.rename new_file
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Do the same for any already compressed files
|
|
173
|
+
prev_compressed_file = LOG_DIR + "#{LOG_FILE_NAME}.#{prev_age}#{BZIPPED_EXTNAME}"
|
|
174
|
+
new_compressed_file = LOG_DIR + "#{LOG_FILE_NAME}.#{age}#{BZIPPED_EXTNAME}"
|
|
175
|
+
if prev_compressed_file.file?
|
|
176
|
+
logger.info "Moving log file #{prev_compressed_file.basename} => #{new_compressed_file.basename}"
|
|
177
|
+
prev_compressed_file.rename new_compressed_file
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
next unless compress_after
|
|
181
|
+
|
|
182
|
+
# compress the one we just moved if we should
|
|
183
|
+
compress_log(new_file) if age >= compress_after && new_file.file?
|
|
184
|
+
end # downto
|
|
185
|
+
|
|
186
|
+
# now for the current logfile...
|
|
187
|
+
rotate_live_log compress_after&.zero?
|
|
188
|
+
|
|
189
|
+
# touch the last rotation file
|
|
190
|
+
LAST_ROTATION_FILE.pix_touch
|
|
191
|
+
rescue => e
|
|
192
|
+
logger.error "Error rotating logs: #{e}"
|
|
193
|
+
e.backtrace.each { |l| logger.error "..#{l}" }
|
|
194
|
+
ensure
|
|
195
|
+
Xolo::Server::Log.rotation_mutex.unlock if Xolo::Server::Log.rotation_mutex.owned?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Rotate the current log file without losing any log entries
|
|
199
|
+
#
|
|
200
|
+
# @return [void]
|
|
201
|
+
###############################
|
|
202
|
+
def self.rotate_live_log(compress_zero)
|
|
203
|
+
# first, delete any old tmp file
|
|
204
|
+
TEMP_LOG_FILE.delete if TEMP_LOG_FILE.file?
|
|
205
|
+
|
|
206
|
+
# then repoint the logger to the temp file
|
|
207
|
+
logger.reopen TEMP_LOG_FILE
|
|
208
|
+
|
|
209
|
+
# make sure it has data in it
|
|
210
|
+
logger.info 'Starting New Log'
|
|
211
|
+
|
|
212
|
+
# then rename the main log to .0
|
|
213
|
+
zero_file = LOG_DIR + "#{LOG_FILE_NAME}.0"
|
|
214
|
+
logger.info "Moving log file #{LOG_FILE_NAME} => #{zero_file.basename}"
|
|
215
|
+
|
|
216
|
+
LOG_FILE.rename zero_file
|
|
217
|
+
|
|
218
|
+
# then rename the temp file to the main log.
|
|
219
|
+
# The logger will still log to it because it holds a
|
|
220
|
+
# filehandle to it, and doesn't care about its name.
|
|
221
|
+
TEMP_LOG_FILE.rename LOG_FILE
|
|
222
|
+
|
|
223
|
+
# compress the zero file if we should
|
|
224
|
+
compress_log(zero_file) if compress_zero
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# should we rotate the logs right now?
|
|
228
|
+
#
|
|
229
|
+
# @return [Boolean] true if we should rotate the logs
|
|
230
|
+
###############################
|
|
231
|
+
def self.rotate_logs_now?(force: false)
|
|
232
|
+
return true if force
|
|
233
|
+
|
|
234
|
+
now = Time.now
|
|
235
|
+
# only during the midnight hour
|
|
236
|
+
return false unless now.hour.zero?
|
|
237
|
+
|
|
238
|
+
# only if the last rotation was more than 23 hrs ago
|
|
239
|
+
# if no rotation_file, assume 24 hrs ago.
|
|
240
|
+
rotation_file = Xolo::Server::Log::LAST_ROTATION_FILE
|
|
241
|
+
last_rotation = rotation_file.file? ? rotation_file.mtime : (now - (24 * 3600))
|
|
242
|
+
|
|
243
|
+
twenty_three_hrs_ago = now - (23 * 3600)
|
|
244
|
+
|
|
245
|
+
# less than (<) means its been more than 23 hrs
|
|
246
|
+
# since the last rotation was before 23 hrs ago.
|
|
247
|
+
last_rotation < twenty_three_hrs_ago
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
#########################
|
|
251
|
+
def self.compress_log(file)
|
|
252
|
+
zipout = `/usr/bin/bzip2 #{file.to_s.shellescape}`
|
|
253
|
+
|
|
254
|
+
if $CHILD_STATUS.success?
|
|
255
|
+
logger.info "Compressed log file: #{file}"
|
|
256
|
+
else
|
|
257
|
+
logger.error "Failed to compress log file: #{file}"
|
|
258
|
+
zipout.lines.each { |l| logger.error ".. #{l.chomp}" }
|
|
259
|
+
end # if success?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
end # module Log
|
|
263
|
+
|
|
264
|
+
# Wrapper for Xolo::Server::Log.logger,
|
|
265
|
+
# also available as Xolo::Server.logger from anywhere.
|
|
266
|
+
# Within Sinatra routes and views, its available via the #logger instance method.
|
|
267
|
+
#########################################
|
|
268
|
+
def self.logger
|
|
269
|
+
Log.logger
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# set the log level of the server logger
|
|
273
|
+
#########################################
|
|
274
|
+
def self.set_log_level(level, admin:)
|
|
275
|
+
# make sure the level is valid
|
|
276
|
+
raise ArgumentError, "Unknown log level '#{level}'" unless Log::LEVELS.include? level.to_s.upcase
|
|
277
|
+
|
|
278
|
+
# unknonwn always gets logged
|
|
279
|
+
logger.unknown "Setting log level to #{level} by #{admin}"
|
|
280
|
+
|
|
281
|
+
logger.level = level
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
end # Server
|
|
285
|
+
|
|
286
|
+
end # module Xolo
|