require 'optparse' module Praefect EXEC_PATH = '/opt/gitlab/embedded/bin/praefect'.freeze DIR_PATH = '/var/opt/gitlab/praefect'.freeze USAGE ||= <<~EOS.freeze Usage: gitlab-ctl praefect command [options] COMMANDS: Repository/Metadata Health remove-repository Remove repository from Gitaly cluster track-repository Tells Gitaly cluster to track a repository track-repositories Track multiple repositories as a single batch list-untracked-repositories Lists repositories that exist on disk but are untracked by Praefect list-storages List virtual storages and their nodes Operational Cluster Health check Runs checks to determine cluster health EOS # These are used arguments to track-repository and listed in the description of --input-path on # track-repositories, requiring different indentation levels. STORAGE_NAME_DESC = <<~EOS.freeze The storage to use as the primary for this repository (mandatory for per_repository elector). %%Repository data on this storage will be used to overwrite corresponding repository data on other %%nodes. EOS VIRTUAL_STORAGE_DESC = <<~EOS.freeze Name of the virtual storage where the repository resides (mandatory). %%The virtual-storage-name can be found in /etc/gitlab/gitlab.rb under praefect["configuration"]["virtual_storage"]. %%If praefect["configuration"]["virtual_storage"] = [{ "name" => "default" , "nodes" => [{ ... }]}, %%{ "name" => "storage_1", "nodes" => [{ ... }]}], the virtual-storage-name would be either "default", or "storage_1". %%This can also be found in the Project Detail page in the Admin Panel under "Gitaly storage name".' EOS RELATIVE_PATH_DESC = <<~EOS.freeze Relative path to the repository on the disk (mandatory). %%These start with @hashed/..." and can be found in the Project Detail page in the Admin Panel under %%"Gitaly relative path"'. EOS SUMMARY_WIDTH = 40 DESC_INDENT = 45 LIST_INDENT = 50 def self.indent(str, len) str.gsub(/%%/, ' ' * len) end def self.parse_options!(args) loop do break if args.shift == 'praefect' end global = OptionParser.new do |opts| opts.on('-h', '--help', 'Usage help') do Kernel.puts USAGE Kernel.exit 0 end end options = {} commands = populate_commands(options) global.order!(args) command = args.shift # common arguments raise OptionParser::ParseError, "Praefect command is not specified." \ if command.nil? || command.empty? raise OptionParser::ParseError, "Unknown Praefect command: #{command}" \ unless commands.key?(command) commands[command].parse!(args) # repository arguments if ['remove-repository', 'track-repository'].include?(command) raise OptionParser::ParseError, "Option --virtual-storage-name must be specified." \ unless options.key?(:virtual_storage_name) raise OptionParser::ParseError, "Option --repository-relative-path must be specified." \ unless options.key?(:repository_relative_path) end raise OptionParser::ParseError, "Option --input-path must be specified." \ if command == 'track-repositories' && !options.key?(:input_path) options[:command] = command options end def self.populate_commands(options) praefect_docs_url = 'https://docs.gitlab.com/ee/administration/gitaly/recovery.html' { 'remove-repository' => OptionParser.new do |opts| opts.banner = "Usage: gitlab-ctl praefect remove-repository [options]. See documentation at #{praefect_docs_url}#manually-remove-repositories" parse_common_options!(options, opts) parse_repository_options!(options, opts) parse_remove_repository_options!(options, opts) end, 'track-repository' => OptionParser.new do |opts| opts.banner = "Usage: gitlab-ctl praefect track-repository [options]. See documentation at #{praefect_docs_url}#manually-add-a-single-repository-to-the-tracking-database" parse_common_options!(options, opts) parse_repository_options!(options, opts) opts.on('--authoritative-storage STORAGE-NAME', indent(STORAGE_NAME_DESC, DESC_INDENT)) do |authoritative_storage| options[:authoritative_storage] = authoritative_storage end parse_replicate_immediately_option!(options, opts) end, 'track-repositories' => OptionParser.new do |opts| opts.banner = "Usage: gitlab-ctl praefect track-repositories [options]. See documentation at #{praefect_docs_url}#manually-add-many-repositories-to-the-tracking-database" parse_common_options!(options, opts) opts.on('--input-path INPUT-PATH', "The path the file containing the list of repositories to be tracked. Must contain a newline-delimited list of JSON objects. Each object must contain the following keys: - relative_path: #{indent(RELATIVE_PATH_DESC, LIST_INDENT).chop} - virtual_storage: #{indent(VIRTUAL_STORAGE_DESC, LIST_INDENT).chop} - authoritative_storage: #{indent(STORAGE_NAME_DESC, LIST_INDENT).chop}") do |input_path| options[:input_path] = input_path end parse_replicate_immediately_option!(options, opts) end, 'list-untracked-repositories' => OptionParser.new do |opts| opts.banner = "Usage: gitlab-ctl praefect list-untracked-repositories [options]. See documentation at #{praefect_docs_url}#list-untracked-repositories" parse_common_options!(options, opts) end, 'check' => OptionParser.new do |opts| opts.banner = "Usage: gitlab-ctl praefect check" parse_common_options!(options, opts) end, 'list-storages' => OptionParser.new do |opts| opts.banner = "Usage: gitlab-ctl praefect list-storages [options]. See documentation at #{praefect_docs_url}#list-virtual-storage-details" parse_common_options!(options, opts) parse_virtual_storage_option!(options, opts) end, } end def self.parse_common_options!(options, option_parser) option_parser.on("-h", "--help", "Prints this help") do option_parser.set_summary_width(SUMMARY_WIDTH) Kernel.puts option_parser Kernel.exit 0 end option_parser.on('--dir DIR', 'Directory in which Praefect is installed') do |dir| options[:dir] = dir end end def self.parse_virtual_storage_option!(options, option_parser) option_parser.on('--virtual-storage-name NAME', indent(VIRTUAL_STORAGE_DESC, DESC_INDENT)) do |virtual_storage_name| options[:virtual_storage_name] = virtual_storage_name end end def self.parse_repository_options!(options, option_parser) parse_virtual_storage_option!(options, option_parser) option_parser.on('--repository-relative-path PATH', indent(RELATIVE_PATH_DESC, DESC_INDENT)) do |repository_relative_path| options[:repository_relative_path] = repository_relative_path end end def self.parse_replicate_immediately_option!(options, option_parser) option_parser.on('--replicate-immediately', "Causes track-repository to replicate the repository to its secondaries immediately. Without this flag, replication jobs will be added to the queue and replication will eventually be executed through Praefect's background process.") do options[:replicate_immediately] = true end end # options specific to remove-repository def self.parse_remove_repository_options!(options, option_parser) option_parser.on('--db-only', 'Remove the repository records from the database only, leaving any the repository on-disk if it exists.') do options[:db_only] = true end option_parser.on('--apply', 'When --apply is used, the repository will be removed from the database and any gitaly nodes on which they reside.') do options[:apply] = true end end def self.set_command(options, config_file_path) # common arguments command = [EXEC_PATH, "-config", config_file_path, options[:command]] # virtual storage argument if ['remove-repository', 'track-repository', 'list-storages'].include?(options[:command]) && options.key?(:virtual_storage_name) command += ["-virtual-storage", options[:virtual_storage_name]] end # repository arguments command += ["-repository", options[:repository_relative_path]] if ['remove-repository', 'track-repository'].include?(options[:command]) # command specific arguments command += ["-authoritative-storage", options[:authoritative_storage]] if options[:command] == 'track-repository' && options.key?(:authoritative_storage) command += ["-input-path", options[:input_path]] if options[:command] == 'track-repositories' && options.key?(:input_path) command += ["-db-only"] if options[:command] == 'remove-repository' && options.key?(:db_only) command += ["-apply"] if options[:command] == 'remove-repository' && options.key?(:apply) # replication argument command += ["-replicate-immediately"] if ['track-repository', 'track-repositories'].include?(options[:command]) && options.key?(:replicate_immediately) command end def self.execute(options) config_file_path = File.join(options.fetch(:dir, DIR_PATH), 'config.toml') [EXEC_PATH, config_file_path].each do |path| next if File.exist?(path) Kernel.abort "Could not find '#{path}' file. Is this command being run on a Praefect node?" end command = set_command(options, config_file_path) status = Kernel.system(*command) Kernel.exit!(1) unless status end end