#!/usr/pkg/bin/ruby # # $Id: vaporadmin 203 2003-06-26 16:45:17Z bolzer $ # Author:: Oliver M. Bolzer (mailto:oliver@fakeroot.net) # Copyright:: (c) Oliver M. Bolzer, 2002 # License:: Distributes under the same terms as Ruby # # Administrative Tool for Repository Management of Vapor require 'vapor' require 'vapor/repositorymgr' require 'rexml/document' unless REXML::VERSION_MAJOR >= 2 and REXML::VERSION_MINOR >= 4 raise LoadError, "vaporadmin requires REXML >= 2.4.0" end begin # only load when available require 'termios' rescue LoadError end # repository specifier is invalid class InvalidRepositoryError < StandardError end # problem with XML-specification class InvalidXMLError < StandardError end # an Vapor::VaporException has properly been handled class HandledVaporException < StandardError end class VaporAdmin include Vapor Commands = ['init','help','add'] Help_Text = Hash.new # initialize with repository-specifier def initialize( rep_spec ) spec = rep_spec.scan(/([\w\.]+)(:[\S]*)?@([\w\.]+)(:[\d]+)?\/(\w+)/).flatten # bark on invalid repository specifier if spec.empty? then raise InvalidRepositoryError, "invalid repository specification: #{rep_spec}" end @username = spec[0] @pass = spec[1] @dbhost = spec[2] @dbport = spec[3] @dbname = spec[4] if !@pass.nil? then @pass.sub!( /^:/, '' ) end if !@dbport.nil? then @dbport.sub!( /^:/, '' ).to_i end end # initialize() # wheter the password is not set yet def need_password? @pass.nil? end # need_password?() # set password def password=( password ) @pass = password end # password=() # output help message def self.help( restargs ) message = Array.new if !restargs.empty? and Help_Text.has_key?( restargs[0] ) then message << Help_Text[ restargs[0] ] else message << "usage: vaporadmin [repository] [args]" message << "Type `vaporadmin help ' for help on a specific command." message << "" message << "All commands except 'help' require a repository specification" message << "in the form of " message << "" message << "Available commands:" Commands.sort.each{|c| message << " " + c } end message( message.join("\n") ) end # self.help() # help message for "help" Help_Text['help'] = ["help: Display usage message.", "usage: vaporadmin help [COMMAND]" ] def execute( command, restargs ) raise TypeError unless command.is_a? String raise TypeError unless restargs.is_a? Array if command == "help" then self.class.help( restargs ) else self.send( command, restargs ) end end # initialize a repository def init( restargs ) db_spec = ["pg", @dbname, @dbhost, @dbport ].compact.join(':') mgr = nil begin mgr = RepositoryManager.new( db_spec, @username, @pass, false ) mgr.init_repository rescue Vapor::RepositoryOfflineError => e error( "Cound not connect to repository: #{e.message}" ) raise HandledVaporException rescue Vapor::BackendInconsistentError => e error( "Could not initialize repository: #{e.message}" ) raise HandledVaporException end end # init() # help message for "init" Help_Text['init'] = ["init: Initialize the Repository.", "usage: vaporadmin REPOSITORY init" ] # add one or more classes to the repository, each argument is the filename # of an XML file containing information about one or more classes def add( restargs ) if restargs.empty? raise InvalidXMLError, "no file specified.\nTry `vaporadmin help add' for usage." end # check existence of all files restargs.each{|filename| if !FileTest.readable?(filename) raise InvalidXMLError, "ERROR: #{filename} not found." end } # connect to repository db_spec = ["pg", @dbname, @dbhost, @dbport ].compact.join(':') @mgr = nil begin @mgr = RepositoryManager.new( db_spec, @username, @pass ) rescue Vapor::RepositoryOfflineError, Vapor::BackendInconsistentError => e error( "Cound not connect to repository, #{e.message}" ) raise HandledVaporException end # parse and process each file add_queue = Array.new restargs.each{|filename| begin File.open( filename ){|file| tree = REXML::Document.new( file ).root add_queue += add_process_xml( tree, filename) } rescue REXML::ParseException => e error( "parse error in #{filename}, #{e.message}" ) raise HandledVaporException rescue InvalidXMLError => e error ( e.message ) raise HandledVaporException end } # start adding the classes deferred_classes = Array.new class_count = 0 @mgr.start_transaction message( "Attempting to add classes to repository:" ) add_queue.each{|klass| begin @mgr.addclass( klass ) message( " #{klass.name}" ) class_count += 1 rescue DuplicateClassError error( "class #{klass.name} already registered with Repository." ) message( "Aborting without saving changes." ) raise HandledVaporException rescue InvalidMetadataError => e error( "while adding #{klass.name}, #{e.message}" ) message( "Aborting without saving changes." ) raise HandledVaporException rescue UnknownSuperclassError deferred_classes << klass rescue Exception => e error( "unrecoverble error #{e.type} while adding #{klass.name}, #{e.message.chomp}" ) message( "Aborting without saving changes." ) raise HandledVaporException end } # sort deferred_classes so that parents come before children that inherit # from them. The superclass is not in the repository and if it's also # not here, we can't know about it => error sorted = Array.new while not deferred_classes.empty? do klass = deferred_classes.shift if sorted.detect{ |k| klass.superclass == k.name } then sorted << klass elsif @mgr.known_classes.detect{ |k| klass.superclass == k.name } then sorted << klass elsif deferred_classes.detect{ |k| klass.superclass == k.name } then deferred_classes << klass else error( "superclass #{klass.superclass} of #{klass.name} not found, aborting." ) raise HandledVaporException end end # add sorted classes to repository sorted.each{|klass| begin @mgr.addclass( klass ) message( " #{klass.name}" ) class_count += 1 rescue DuplicateClassError error( "class #{klass.name} already registered with Repository, aborting." ) raise HandledVaporException rescue UnknownSuperclassError error( "superclass #{klass.superclass} not found anywhere, aborting" ) raise HandledVaporException end } @mgr.commit_transaction message( "Added #{class_count} new classes to Repository." ) end # add() Help_Text['add'] = ["add: Add one or more classes to the Repository.", "usage: vaporadmin REPOSITORY init XML-FILE [XML-FILE...]" ] # extract all classes from a XML-Tree def add_process_xml( tree, filename, namespace = '' ) # check top-level tag if tree.local_name != 'vapor' and tree.local_name != 'module' then raise InvalidXMLException, "expecting top-level element but was <#{tree.local_name}> in #{filename}" end generated_classes = Array.new # decend into content tree.each_element{|element| case element.local_name when 'class' if element.attributes['name'].nil? then raise InvalidXMLError, "mission name for in #{filename}" end # convert into ClassMetaData generated_classes << add_generate_class( element, namespace ) when 'module' generated_classes += add_process_xml( element, filename, namespace + element.attributes['name'] + '::' ) else raise InvalidXMLError, "Unknwon XML element <#{element.local_name}> where or expected in #{filename}" end } return generated_classes end # add_process_xml() private :add_process_xml # convert an into ClassMetaData while watching for Repository # consistency def add_generate_class( element, namespace = '' ) raise TypeError unless element.is_a? REXML::Element raise TypeError unless namespace.is_a? String name = namespace + element.attributes['name'] superclass = element.attributes['superclass'] klass = ClassMetaData.new( name, superclass, name.to_s ) # collect attributes element.each_element('attribute'){|attr| aname = attr.attributes['name'] if aname.nil? then raise InvalidXMLError, "missing name of attribute in definition of #{name}." end is_array = case attr.attributes['is_array'] when 'true' then true when 'false', nil then false else raise InvalidXMLError, "invalid Array specification #{attr.attributes['is_array']} for #{name}.#{aname}" end type = case attr.attributes['type'] when 'String' then ClassAttribute::String when 'Integer' then ClassAttribute::Integer when 'Date' then ClassAttribute::Date when 'Reference' then ClassAttribute::Reference when 'Float' then ClassAttribute::Float when 'Boolean' then ClassAttribute::Boolean else raise InvalidXMLError, "invalid type #{attr.attributes['type']} for #{name}.#{aname}" end klass.attributes << ClassAttribute.new( aname, type, is_array ) indexed = case attr.attributes['index'] when 'true' then true when 'false',nil then false else raise InvalidXMLError, "invalid value #{attr.attributes['index']} for #{name}.#{aname}" end if indexed then klass.indexes << [ aname ] end unique = case attr.attributes['unique'] when 'true' then true when 'false',nil then false else aise InvalidXMLError, "invalid value #{attr.attributes['unique']} for #{name}.#{aname}" end if unique then klass.unique << [ aname ] end } # look for indexes element.each_element('index'){|index| index_attributes = Array.new index.each_element('attribute'){|attr| aname = attr.attributes['name'] if aname.nil? then raise InvalidXMLError, "missing name of attribute in index definition of #{name}." end index_attributes << aname } klass.indexes << index_attributes } # look for uniqueness constraints element.each_element('unique'){|uniq| unique_attributes = Array.new uniq.each_element('attribute'){|attr| aname = attr.attributes['name'] if aname.nil? then raise InvalidXMLError, "missing name of attribute in uniqueness constraint of #{name}." end unique_attributes << aname } klass.unique << unique_attributes } # done return klass end # add_generate_class() private :add_generate_class end # class VaporAdmin # output error message to standard error def error( message ) STDERR.write( "ERROR: " + message ) STDERR.write( "\n" ) end # error() # output a warning message to standard error def warn( message ) STDERR.write( message ) STDERR.write( "\n" ) end # output a normal message to standard error def message( message ) STDOUT.write( message ) STDOUT.write( "\n" ) end # message() ################ main ################### first_argument = ARGV.shift db_spec = '' # check if first argument is "help" or repository specifier case first_argument when nil # no option given warn( "Try `vaporadmin help' for usage." ) exit 1 when "help" # help requested VaporAdmin.help( ARGV ) exit 0 else db_spec = first_argument end # check if command is valid command = ARGV.shift if !VaporAdmin::Commands.include? command then warn( "unknown command: #{command}" ) warn( "Try `vaporadmin help' for usage." ) exit 1 end # escape route for "help" if command == "help" then VaporAdmin.help( ARGV ) exit 0 end # initialize VaporAdmin admin = nil begin admin = VaporAdmin.new( db_spec ) rescue InvalidRepositoryError => e warn( e.message ) warn( "Try `vaporadmin help' for usage." ) exit 1 end # ask user for password if needed if admin.need_password? then password = nil # set screen property to noecho if Termios present if defined? Termios then oldattr = Termios.getattr(STDIN) newattr = oldattr.dup newattr.c_lflag &= ~Termios::ECHO Termios.setattr( STDIN, Termios::TCSANOW, newattr) else warn( "WARNING: Your password will be echoed on screen." ) warn( " Install the Termios module for Ruby to surpress screen echo." ) end begin # read password while password.nil? do STDOUT << "Password: " password = STDIN.gets password.chomp! STDOUT << "\n" end admin.password = password rescue Interrupt exit 130 # exit code for SIGINT ensure # restore screen echo if defined? Termios then Termios.setattr( STDIN, Termios::TCSANOW, oldattr ) end end end # execute action begin admin.execute( command, ARGV ) rescue HandledVaporException exit 1 rescue Interrupt exut 130 end exit 0