#!/usr/bin/ruby -w ############################################################################## # tpkg package management and deployment tool ############################################################################## # Ensure we can find tpkg.rb $:.unshift File.join(File.dirname(__FILE__), "..", "lib") require 'optparse' require 'tpkg' # # Parse the command line options # @action = nil @action_value = nil @debug = false @prompt = true @quiet = false @sudo = true @lockforce = false @force = false @deploy = false @deploy_params = ARGV # hold parameters for how to invoke tpkg on the machines we're deploying to @deploy_options = {} # options for how to run the deployer @servers = nil @worker_count = 10 @rerun_with_sudo = false @tpkg_options = {} # options for instantiating Tpkg object @init_options = {} # options for how to run init scripts @other_options = {} @compress = "gzip" def rerun_with_sudo_if_necessary if Process.euid != 0 && @sudo warn "Executing with sudo" # Depending on how sudo is configured it might remove TPKG_HOME from the # environment. As such we set the base as a command line option to ensure # it survives the sudo process. if ENV['TPKG_HOME'] exec('sudo', $0, '--base', ENV['TPKG_HOME'], *ARGV) else exec('sudo', $0, *ARGV) end end end opts = OptionParser.new(nil, 24, ' ') opts.banner = 'Usage: tpkg [options]' opts.on('--servers', '-s', '=SERVERS', Array, 'Servers to apply the actions to') do |opt| @servers = opt @deploy = true @deploy_params = @deploy_params - ['--servers', '-s', @servers.join(","), "--servers=#{@servers.join(',')}"] end opts.on('--make', '-m', '=DIRECTORY', 'Make a package out of the contents of the directory') do |opt| @action = :make @action_value = opt end opts.on('--extract', '-x', '=DIRECTORY', 'Extract the metadata for a directory of packages') do |opt| @action = :extract @action_value = opt end installexample = " --install pkgname=version=package_version\n (Example: tpkg --install hive=2.1) Will install hive version 2.1" opts.on('--install', '-i', '=PACKAGES', "Install one or more packages\n#{installexample}", Array) do |opt| @rerun_with_sudo = true @action = :install @action_value = opt end opts.on('--upgrade', '-u', '=PACKAGES', 'Upgrade one or more packages', Array) do |opt| @rerun_with_sudo = true @action = :upgrade @action_value = opt end opts.on('--downgrade', '=PACKAGES', 'Downgrade one or more packages', Array) do |opt| @other_options[:downgrade] = true @rerun_with_sudo = true @action = :upgrade @action_value = opt end opts.on('--ua', 'Upgrade all packages') do |opt| @rerun_with_sudo = true @action = :upgrade end opts.on('--remove', '-r', '=PACKAGES', 'Remove one or more packages', Array) do |opt| @rerun_with_sudo = true @action = :remove @action_value = opt end opts.on('--rd', '=PACKAGES', 'Similar to -r but also remove depending packages', Array) do |opt| @rerun_with_sudo = true @other_options[:remove_all_dep] = true @action = :remove @action_value = opt end opts.on('--rp', '=PACKAGES', 'Similar to -r but also remove prerequisites', Array) do |opt| @rerun_with_sudo = true @other_options[:remove_all_prereq] = true @action = :remove @action_value = opt end opts.on('--ra', 'Remove all packages') do |opt| @rerun_with_sudo = true @action = :remove end opts.on('--verify', '-V', '=NAME', 'Verify packages') do |opt| @rerun_with_sudo = true @action = :verify @action_value = opt end opts.on('--start', '=NAME', 'Start the init script for specified package', Array) do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:packages] = opt @init_options[:cmd] = 'start' end opts.on('--stop', '=NAME', 'Stop the init script for specified package', Array) do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:packages] = opt @init_options[:cmd] = 'stop' end opts.on('--restart', '=NAME', 'Restart the init script for specified package', Array) do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:packages] = opt @init_options[:cmd] = 'restart' end opts.on('--reload', '=NAME', 'Reload the init script for specified package', Array) do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:packages] = opt @init_options[:cmd] = 'reload' end opts.on('--status', '=NAME', 'Get status from init script for specified package', Array) do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:packages] = opt @init_options[:cmd] = 'status' end opts.on('--start-all', 'Start the init scripts for all packages') do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:cmd] = 'start' end opts.on('--stop-all', 'Stop the init script for all packages') do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:cmd] = 'stop' end opts.on('--restart-all', 'Restart the init script for all packages') do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:cmd] = 'restart' end opts.on('--reload-all', 'Reload the init script for all packages') do |opt| @rerun_with_sudo = true @action = :execute_init @init_options[:cmd] = 'reload' end opts.on('--exec-init', '=NAME', 'Execute init scripts for specified packages', Array) do |opt| @rerun_with_sudo = true @init_options[:packages] = opt @action = :execute_init end opts.on('--init-script', '=NAME', 'What init scripts to execute', Array) do |opt| @rerun_with_sudo = true @init_options[:scripts] = opt end opts.on('--init-cmd', '=CMD', 'Invoke specified init script command') do |opt| @rerun_with_sudo = true @init_options[:cmd] = opt end opts.on('--query', '-q', '=NAMES', 'List installed packages', Array) do |opt| # People mistype -qa instead of --qa frequently if opt == ['a'] warn "NOTE: tpkg -qa queries for a pkg named 'a', you probably want --qa for all pkgs" end @action = :query_installed @action_value = opt end opts.on('--qa', 'List all installed packages') do |opt| @action = :query_installed end opts.on('--qi', '=NAME', 'Info for packages') do |opt| @action = :query_info @action_value = opt end opts.on('--ql', '=NAME', 'List files in installed packages') do |opt| @action = :query_list_files @action_value = opt end opts.on('--qf', '=FILE', 'List the package that owns a file') do |opt| @action = :query_who_owns_file @action_value = opt end opts.on('--qv', '=NAME', 'List available packages') do |opt| @action = :query_available @action_value = opt end opts.on('--qva', 'List all available packages') do |opt| @action = :query_available end opts.on('--qr', '=NAME', 'List installed packages that require package') do |opt| @action = :query_requires @action_value = opt end opts.on('--qd', '=NAME', 'List the packages that package depends on') do |opt| @action = :query_depends @action_value = opt end opts.on('--qld', '=NAME', 'Similar to --qd, but only look at local packages') do |opt| @action = :query_local_depends @action_value = opt end opts.on('--dw', '=INTEGER', 'Number of workers for deploying') do |opt| @worker_count = opt.to_i @deploy_params = @deploy_params - ['--dw', @worker_count, "--dw=#{opt}"] end opts.on('--qX', '=FILENAME', 'Display tpkg.xml or tpkg.yml of the given package') do |opt| @action = :query_tpkg_metadata @action_value = opt end opts.on('--history', 'Display package installation history') do |opt| @action = :query_history end opts.on('--qenv', "Display machine's information") do |opt| @action = :query_env end opts.on('--qconf', "Display tpkg's configuration settings") do |opt| @action = :query_conf end opts.on('--base', '=BASE', 'Base directory for tpkg operations') do |opt| @tpkg_options[:base] = opt end opts.on('--source', '=NAME', 'Sources where packages are located', Array) do |opt| @tpkg_options[:sources] = opt end opts.on('--download', '=PACKAGES', 'Download one or more packages', Array) do |opt| @action = :download @action_value = opt end opts.on('-n', '--no-prompt', 'No confirmation prompts') do |opt| @prompt = opt Tpkg::set_prompt(@prompt) end opts.on('--quiet', 'Reduce informative but non-essential output') do |opt| @quiet = opt end opts.on('--no-sudo', 'No calls to sudo for operations that might need root') do |opt| @sudo = opt end opts.on('--lock-force', 'Force the removal of an existing lockfile') do |opt| @lockforce = opt end opts.on('--force-replace', 'Replace conflicting pkgs with the new one(s)') do |opt| @other_options[:force_replace] = opt end opts.on('--force', 'Force the execution of a given task') do |opt| @force = opt end opts.on('-o', '--out', '=DIR', 'Output directory for the -m option') do |opt| @other_options[:out] = opt end opts.on('--use-ssh-key [FILE]', 'Use ssh key for deploying instead of password') do |opt| @deploy_options["use-ssh-key"] = true @deploy_options["ssh-key"] = opt @deploy_params = @deploy_params - ['--use-ssh-key', opt, "--use-ssh-key=#{opt}"] end opts.on('--deploy-as', '=USERNAME', 'What username to use for deploying to remote server') do |opt| @deploy_options["deploy-as"] = opt @deploy_params = @deploy_params - ['--deploy-as'] end opts.on('--compress', '=[TYPE]', 'Compress files when making packages') do |opt| if opt == "no" @compress= false else @compress = opt end end opts.on('--test-root TESTDIR', 'For use by the test suite only.') do |opt| @tpkg_options[:file_system_root] = opt end opts.on('--debug', 'Print lots of messages about what tpkg is doing') do |opt| @debug = opt Tpkg::set_debug(@debug) end opts.on('--version', 'Show tpkg version') do |opt| @action = :query_version end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end leftovers = opts.parse(ARGV) # Rerun with sudo if necessary, unless it's a deploy, then # we don't need to run with sudo on this machine. It will run with sudo # on the remote machine if @rerun_with_sudo && !@deploy rerun_with_sudo_if_necessary end # Display a usage message if the user did not specify a valid action to perform. if !@action puts opts exit end # # Figure out base directory, sources and other configuration # def instantiate_tpkg(options = {}) base = options[:base] sources = options[:sources] || [] report_server = nil # base can come from four possible places. They take precedence in this # order: # - command line option # - TPKG_HOME environment variable # - config file # - Tpkg::DEFAULT_BASE if ENV['TPKG_HOME'] if !base base = ENV['TPKG_HOME'] # Warn the user, as this could potentially be confusing # if they don't realize there's an environment variable set. warn "Using base '#{base}' base from $TPKG_HOME" else warn "Ignoring TPKG_HOME" if @debug end end # FIXME: Move config file parsing to tpkg.rb # http://sourceforge.net/apps/trac/tpkg/ticket/28 fsroot = options[:file_system_root] ? options[:file_system_root] : '' [File.join(fsroot, Tpkg::DEFAULT_CONFIGDIR, 'tpkg.conf'), File.join(fsroot, ENV['HOME'], ".tpkg.conf")].each do |configfile| if File.exist?(configfile) IO.foreach(configfile) do |line| line.chomp! next if (line =~ /^\s*$/); # Skip blank lines next if (line =~ /^\s*#/); # Skip comments line.strip! # Remove leading/trailing whitespace key, value = line.split(/\s*=\s*/, 2) if key == 'base' if !base # Warn the user, as this could potentially be confusing # if they don't realize there's a config file lying # around base = value warn "Using base #{base} from #{configfile}" else warn "Ignoring 'base' option in #{@configfile}" if @debug end elsif key == 'source' sources << value puts "Loaded source #{value} from #{configfile}" if (@debug) elsif key == 'report_server' report_server = value puts "Loaded report server #{report_server} from #{configfile}" if (@debug) end end end end if !base base = Tpkg::DEFAULT_BASE end if !@sudo curruid = Process.euid if curruid == 0 # Besides there being no point to running with --no-sudo when root, we # don't want users to accidentally create files/directories that can't be # modified by other users who properly run --no-sudo as a regular user. raise "--no-sudo cannot be used as 'root' user or via sudo" end baseuid = File.stat(base).uid # We want to ensure that all --no-sudo usage within a given base directory # is done under the same account. if baseuid != curruid raise "Base dir #{base} owned by UID #{baseuid}, not your UID #{curruid}" end end # FIXME: This is ugly. We would set the appropriate things in options and # call Tpkg.new(options) tpkg = Tpkg.new(:file_system_root => options[:file_system_root], :base => base, :sources => sources, :report_server => report_server, :lockforce => @lockforce, :force => @force, :sudo => @sudo) end passphrase_callback = lambda do | package | # ask("Passphrase for #{package}: ", true) begin system 'stty -echo;' print "Passphrase for #{package}: " input = STDIN.gets.chomp ensure system 'stty echo; echo ""' end input end # # Do stuff # if @deploy # puts "Creating deployer with #{@worker_count} number of worker" @deploy_options["max-worker"] = @worker_count @deploy_options["abort-on-fail"] = false # Check to see if ssh-key is accessible # Net::SSH doesn't warn the user about problems with the key file # (i.e. if it doesn't exist or isn't readable), resulting in the user # just getting a generic "Bad username/password combination" error # message because authentication fails. As such we do some of our own # checking and warning here so that the user gets a more specific # error message. if @deploy_options["use-ssh-key"] && @deploy_options["ssh-key"] ssh_key = @deploy_options["ssh-key"] if !File.readable?(ssh_key) && (Process.euid == 0 || !@sudo) raise "Unable to read ssh key from #{ssh_key}" elsif !File.readable?(ssh_key) warn "Warning: Unable to read ssh key from #{ssh_key}. Attempting to rerun tpkg with sudo." rerun_with_sudo_if_necessary end end Tpkg::deploy(@deploy_params, @deploy_options, @servers) exit end if @action_value.is_a?(Array) @action_value.uniq! end # tell tpkg not to prompt if stdin is not tty if !$stdin.tty? Tpkg::set_prompt(false) end ret_val = 0 case @action when :make @other_options[:force] = @force @other_options[:compress] = @compress pkgfile = Tpkg::make_package(@action_value, passphrase_callback, @other_options) if pkgfile puts "Package is #{pkgfile}" else puts "Package build aborted or failed" end when :extract Tpkg::extract_metadata(@action_value) when :install tpkg = instantiate_tpkg(@tpkg_options) ret_val = tpkg.install(@action_value, passphrase_callback, @other_options) when :upgrade tpkg = instantiate_tpkg(@tpkg_options) ret_val = tpkg.upgrade(@action_value, passphrase_callback, @other_options) when :remove tpkg = instantiate_tpkg(@tpkg_options) ret_val = tpkg.remove(@action_value, @other_options) when :download tpkg = instantiate_tpkg(@tpkg_options) ret_val = tpkg.download_pkgs(@action_value, @other_options) when :verify result = nil # Verify a given .tpkg file if File.exist?(@action_value) Tpkg::verify_package_checksum(@action_value) # Verify an installed pkg else tpkg = instantiate_tpkg(@tpkg_options) results = tpkg.verify_file_metadata([@action_value]) if results.length == 0 puts "No package found" end success = true results.each do | file, errors | if errors.length == 0 puts "#{file}: Passed" else puts "#{file}: Failed (Reasons: #{errors.join(", ")})" success = false end end puts "Package verification failed" unless success end when :execute_init tpkg = instantiate_tpkg(@tpkg_options) if @init_options[:cmd].nil? raise "You didn't specify what init command to run" end ret_val = tpkg.execute_init(@init_options) when :query_installed tpkg = instantiate_tpkg(@tpkg_options) req = nil matches = [] if @action_value @action_value.each do | value | req = Tpkg::parse_request(value, tpkg.installed_directory) match = tpkg.installed_packages_that_meet_requirement(req) # If the user requested specific packages and we found no matches # then exit with a non-zero value to indicate failure. This allows # command-line syntax like "tpkg -q foo || tpkg -i foo" to ensure # that a package is installed. ret_val = 1 if match.empty? matches |= match end else matches = tpkg.installed_packages_that_meet_requirement(req) end if !@quiet matches.sort(&Tpkg::SORT_PACKAGES).each do |pkg| puts pkg[:metadata][:filename] end end when :query_info metadatas = nil if File.exist?(@action_value) metadatas = [Tpkg::metadata_from_package(@action_value)] else tpkg = instantiate_tpkg(@tpkg_options) metadatas = [] requirements = [] packages = {} tpkg.parse_requests([@action_value], requirements, packages) packages.each do | name, pkgs | pkgs.each do | pkg | metadatas << pkg[:metadata] end end end already_displayed = {} metadatas.each do |metadata| next if already_displayed[metadata[:filename]] already_displayed[metadata[:filename]] = true [:name, :version, :package_version, :operatingsystem, :architecture, :maintainer, :description, :bugreporting].each do |field| metadata[field] = 'any' if field == :operatingsystem && metadata[field].nil? metadata[field] = 'any' if field == :architecture && metadata[field].nil? if metadata[field] if metadata[field].kind_of?(Array) puts "#{field}: #{metadata[field].join(',')}" else puts "#{field}: #{metadata[field]}" end end end tpkg_version = metadata[:tpkg_version] || "< 1.26.1" puts "This package was built with tpkg version #{tpkg_version}." if metadata[:dependencies] puts "This package depends on other packages, use --qd/--qld to view the dependencies." end puts "================================================================================" end when :query_list_files tpkg = instantiate_tpkg(@tpkg_options) pkgfiles = nil if File.exist?(@action_value) fip = Tpkg::files_in_package(@action_value) tpkg.normalize_paths(fip) puts "#{@action_value}:" fip[:normalized].each { |file| puts file } else pkgfiles = [] metadatas = [] requirements = [] packages = {} req = Tpkg::parse_request(@action_value) pkgs = tpkg.installed_packages_that_meet_requirement(req) if pkgs.nil? or pkgs.empty? ret_val = 1 puts "Could not find any installed packages that meet the request \"#{@action_value}\"" end pkgs.each do | pkg | pkgfiles << pkg[:metadata][:filename] end files = tpkg.files_for_installed_packages(pkgfiles) files.each do |pkgfile, fip| puts "#{pkgfile}:" fip[:normalized].each { |file| puts file } end end when :query_who_owns_file tpkg = instantiate_tpkg(@tpkg_options) tpkg.files_for_installed_packages.each do |pkgfile, fip| fip[:normalized].each do |file| if file == File.expand_path(@action_value) puts "#{file}: #{pkgfile}" end end end when :query_available tpkg = instantiate_tpkg(@tpkg_options) req = nil if @action_value req = Tpkg::parse_request(@action_value) end tpkg.available_packages_that_meet_requirement(req).each do |pkg| next if pkg[:source] == :native_installed next if pkg[:source] == :native_available puts "#{pkg[:metadata][:filename]} (#{pkg[:source]})" end when :query_requires tpkg = instantiate_tpkg(@tpkg_options) # parse the request requirements = [] packages = {} tpkg.parse_requests([@action_value], requirements, packages) # get dependencies of all installed packages dependencies = {} tpkg.metadata_for_installed_packages.each do |metadata| dependencies[metadata[:filename]] = metadata[:dependencies] end # check to see if the any required dependencies match with what the # user specified in the request packages.each do |name, pkgs| pkgs.each do |pkg| next if pkg[:source] != :currently_installed puts "The following package(s) require #{pkg[:metadata][:filename]}:" dependencies.each do | requiree, deps | next if deps.nil? deps.each do | dep | if Tpkg::package_meets_requirement?(pkg, dep) puts " #{requiree}" end end end end end when :query_local_depends tpkg = instantiate_tpkg(@tpkg_options) req = Tpkg::parse_request(@action_value) pkgs = tpkg.installed_packages_that_meet_requirement(req) pkgs.each do | pkg | puts pkg[:metadata][:filename] + ':' if pkg[:metadata][:dependencies] pkg[:metadata][:dependencies].each do |req| puts " Requires #{req[:name]}" req.each do |field, value| next if field == :name puts " #{field}: #{value}" end end end end when :query_depends tpkg = instantiate_tpkg(@tpkg_options) requirements = [] packages = {} tpkg.parse_requests([@action_value], requirements, packages) packages.each do |name, pkgs| already_displayed = {} pkgs.each do |pkg| # parse_requests returns both installed and available packages. # The same package may show up in both, skip any duplicates. next if already_displayed[pkg[:filename]] already_displayed[pkg[:metadata][:filename]] = true puts pkg[:metadata][:filename] + ':' if pkg[:metadata][:dependencies] pkg[:metadata][:dependencies].each do |req| puts " Requires #{req[:name]}" req.each do |field, value| next if field == :name puts " #{field}: #{value}" end end end end end when :query_tpkg_metadata tpkg = instantiate_tpkg(@tpkg_options) if File.exist?(@action_value) puts Tpkg::extract_tpkg_metadata_file(@action_value) elsif File.exists?(File.join(tpkg.installed_directory, @action_value)) puts Tpkg::extract_tpkg_metadata_file(File.join(tpkg.installed_directory, @action_value)) else puts "File #{@action_value} doesn't exist." end when :query_env puts "Operating System: #{Tpkg::get_os}" puts "Architecture: #{Tpkg::get_arch}" when :query_conf # This is somewhat arbitrarily limited to the options read from the # tpkg.conf config files. The reason it exists at all is that it is # difficult for users to programatically find out what these will be set to # without recreating all of our logic about deciding which config files to # read, which order to read them in, what environment variables override the # config files, etc. tpkg = instantiate_tpkg(@tpkg_options) puts "Base: #{tpkg.base}" puts "Sources: #{tpkg.sources.inspect}" puts "Report server: #{tpkg.report_server}" when :query_history tpkg = instantiate_tpkg(@tpkg_options) tpkg.installation_history when :query_version puts Tpkg::VERSION end exit ret_val