files/gitlab-ctl-commands/lib/gitlab_ctl/pg_upgrade.rb (188 lines of code) (raw):
require 'optparse'
require 'mixlib/shellout'
require 'rainbow'
require_relative 'util'
require_relative '../gitlab_ctl'
# For testing purposes, if the first path cannot be found load the second
begin
require_relative '../../../../cookbooks/gitlab/libraries/pg_version'
rescue LoadError
require_relative '../../../gitlab-cookbooks/gitlab/libraries/pg_version'
end
module GitlabCtl
class PgUpgrade
include GitlabCtl::Util
attr_accessor :base_path, :data_path, :tmp_dir, :timeout, :target_version, :initial_version, :psql_command, :port
attr_writer :data_dir, :tmp_data_dir
def initialize(base_path, data_path, target_version, tmp_dir = nil, timeout = nil, psql_command = nil, port = nil)
@base_path = base_path
@data_path = data_path
@tmp_dir = tmp_dir
@timeout = timeout
@target_version = target_version
@initial_version = fetch_running_version
@port = port || public_node_attributes['postgresql']['port']
@psql_command = psql_command || "gitlab-psql"
end
def data_dir
return @data_dir if @data_dir
# We still need to support legacy attributes starting with `gitlab`, as
# they might exists before running configure on an existing installation
pg_base_dir = node_attributes.dig(:gitlab, :postgresql, :dir) || node_attributes.dig(:postgresql, :dir) || File.join(@data_path, "postgresql")
@data_dir = File.join(pg_base_dir, "data")
@data_dir = File.realpath(@data_dir) if File.exist?(@data_dir)
@data_dir
end
def tmp_data_dir
return @tmp_data_dir if @tmp_data_dir
@tmp_data_dir = @tmp_dir ? "#{@tmp_dir}/data" : data_dir
end
def enough_free_space?(dir, needed)
needed <= space_free(dir)
end
def space_needed(dir)
GitlabCtl::Util.get_command_output(
"du -s --block-size=1m #{dir}", nil, @timeout
).split.first.to_i
end
def space_free(dir)
space_available = GitlabCtl::Util.get_command_output(
"df -P --block-size=1m #{dir} | awk '{print $4}'", nil, @timeout
).split.last.to_i
(space_available * 0.9).to_i
end
def run_pg_command(command)
pg_username = node_attributes.dig(:postgresql, :username)
GitlabCtl::Util.get_command_output("su - #{pg_username} -c \"#{command}\"", nil, @timeout)
end
def fetch_running_version
PGVersion.parse(GitlabCtl::Util.get_command_output(
"#{@base_path}/embedded/bin/pg_ctl --version"
).split.last)
end
def run_query(query)
GitlabCtl::Util.get_command_output(
"#{@psql_command} -d postgres -c '#{query}' -q -t",
nil, # user
@timeout
).strip
end
def fetch_lc_collate
run_query('SHOW LC_COLLATE')
end
def fetch_lc_ctype
run_query('SHOW LC_CTYPE')
end
def fetch_server_encoding
run_query('SHOW SERVER_ENCODING')
end
def fetch_data_version
PGVersion.parse(File.read("#{data_dir}/PG_VERSION").strip)
end
def running?(service = 'postgresql')
!GitlabCtl::Util.run_command("gitlab-ctl status #{service}").error?
end
def start(service = 'postgresql')
GitlabCtl::Util.run_command("gitlab-ctl start #{service}").error!
end
def node_attributes
@node_attributes ||= GitlabCtl::Util.get_node_attributes(@base_path)
end
def public_node_attributes
@public_node_attributes ||= GitlabCtl::Util.get_public_node_attributes
end
def base_postgresql_path
"#{base_path}/embedded/postgresql"
end
def target_version_path
"#{base_postgresql_path}/#{target_version.major}"
end
def initial_version_path
"#{base_postgresql_path}/#{initial_version.major}"
end
def upgrade_artifact_exists?(path)
return false unless File.exist?(path)
!Dir.empty?(path)
end
def log(message)
$stderr.puts message
end
def run_pg_upgrade
unless GitlabCtl::Util.progress_message('Upgrading the data') do
begin
run_pg_command(
"#{target_version_path}/bin/pg_upgrade " \
"-b #{initial_version_path}/bin " \
"--old-datadir=#{data_dir} " \
"--new-datadir=#{tmp_data_dir}.#{target_version.major} " \
"-B #{target_version_path}/bin "
)
rescue GitlabCtl::Errors::ExecutionError => e
log "Error upgrading the data to version #{target_version}"
log "STDOUT: #{e.stdout}"
log "STDERR: #{e.stderr}"
false
rescue Mixlib::ShellOut::CommandTimeout
log ""
log "Timed out during the database upgrade.".color(:red)
log "To run with more time, remove the temporary directory #{tmp_data_dir}.#{target_version.major},".color(:red)
log "then re-run your previous command, adding the --timeout option.".color(:red)
log "See the docs for more information: https://docs.gitlab.com/omnibus/settings/database.html#upgrade-packaged-postgresql-server".color(:red)
log "Or run gitlab-ctl pg-upgrade --help for usage".color(:red)
false
end
end
raise GitlabCtl::Errors::ExecutionError.new(
'run_pg_upgrade', '', 'Error upgrading the database'
)
end
end
class << self
def parse_options(args)
options = {
tmp_dir: nil,
wait: true,
skip_unregister: false,
timeout: nil,
target_version: nil,
skip_disk_check: false,
leader: nil,
replica: nil,
standby_leader: nil
}
OptionParser.new do |opts|
opts.on('-tDIR', '--tmp-dir=DIR', 'Storage location for temporary data') do |t|
options[:tmp_dir] = t
end
opts.on('-w', '--no-wait', 'Do not wait before starting the upgrade process') do
options[:wait] = false
end
opts.on('-s', '--skip-unregister', 'Skip the attempt to unregister an HA secondary node. No-op in non-HA scenarios') do
options[:skip_unregister] = true
end
opts.on('-TTIMEOUT', '--timeout=TIMEOUT', 'Timeout in milliseconds for the execution of the underlying commands. Accepts duration format such as 1d2h3m4s5ms.') do |t|
i = GitlabCtl::Util.parse_duration(t)
options[:timeout] = i.positive? ? i : nil
end
opts.on('-VVERSION', '--target-version=VERSION', 'The explicit major version to upgrade or downgrade to') do |v|
options[:target_version] = v
end
opts.on('--skip-disk-check', 'Skip checking that there is enough free disk space to perform upgrade') do
options[:skip_disk_check] = true
end
opts.on('--leader', 'Patroni only. Force leader upgrade procedure.') do
options[:leader] = true
options[:replica] = false
end
opts.on('--replica', 'Patroni only. Force replica upgrade procedure.') do
options[:replica] = true
options[:leader] = false
end
opts.on('--standby-leader', 'Patroni only. Force standby-leader upgrade procedure.') do
options[:leader] = false
options[:replica] = false
options[:standby_leader] = true
end
end.parse!(args)
options
end
end
end
end