#!/usr/bin/perl
# $Id: cfmdb,v 1.62 2000/06/01 02:24:04 dmetz Exp $
# $crtd:  by  Derald Metzger  on  200307 $
# $cmnt:  cfmdb cmd. Does global management of the cfm database.
#                Copyright (c) 1998 - 2000 Derald Metzger
# This file is part of the cfm pkg (GPL).
# $

use strict;

#  Load perl modules
use Cfm;
use FileHandle;
use File::Basename;
use File::Copy;
use File::Find;
use File::Path;
use File::Spec::Unix;
use Getopt::Long;


#  Var declarations
use vars '$opt_a';       # all flag, or add flag for hostset mode
use vars '$opt_counts';  # Counts flag, output --cr, --cs, & ci counts
use vars '$opt_d';       # Delete flag, delete host set
use vars '$opt_da';      # Delete-add flag, replace host's set list
use vars '$opt_erase';   # Erase mode, erase something in the cfmdb
use vars '$opt_file';    # File flag, query or erase file arg
use vars '$opt_hostset'; # Pseudo flag, hostset mode. Host arg, no mode set
use vars '$opt_initdb';  # Initdb mode, create new cfmdb admin dirtree
use vars '$opt_init';    # Init mode, init hosts or sets (explicit or rpm)
use vars '$opt_host';    # Host flag, get host name args
#                        #   Note: getting an arg with $opt_host
#                        #         set and no mode set => hostset mode
use vars '$opt_list';    # List files or sets for query
use vars '$opt_nefile';  # Non-existant file flag
use vars '$opt_neslnk';  # Non-existant symlink flag
use vars '$opt_query';   # Query mode, output info about cfmdb
use vars '$opt_rpm';     # Rpm flag, get rpm name args
use vars '$opt_set';     # Set flag, get set name args
use vars '$opt_version'; # Print the version info

#  Id/set cfmdb vars
my $admdir = "$Cfm::cfmroot/CFMROOT";   # Cfm admin dir
my @admfiles=("allfiles","config","hostsets","hsfiles","rpmfixes"); # Adm files
my $allfilesfn = "$admdir/allfiles";    # Cfm main config file
my @args;                               # System call args
my $brch; # Selected set tag's branch  (<branch>.<level>)
my $configfn = "$admdir/config";        # Cfm main config file
my $dbf;                                # Cfmdb node name
my $err_cnt = 0;                        # Err count
my $fil_cnt = 0;                        # File count
my %file_dat;                           # <filename>->[ $set_cnt,<[setnames]>]]
my $fn;   # Canonical form of input file arg, no .cfmsl sfx
my $nd;                                 # Index for @nodes
my @nodes;                   # Multidimension arrray of filesystem nodes info
my $hfn;  # Host side filename with possible .cfmsl suffix for symlinks
my $hnam = $Cfm::uname[1];              # Executing host's name
my $host = $hnam;                       # Host for which mods are being made
my @hosts;                              # List of host args
my @hostsets;                           # Content of cfm hostsets file
my $hostsetsfn = "$admdir/hostsets";    # Host's sets file
my $inst_cnt = 0;                       # instance count of in use file vers
my $hsfilesfn = "$admdir/hsfiles";      # Host specific files list
my $lvl;                     # Selected set tag's level   (1.1.x.<level>)
my $osvdir = "$Cfm::cfmroot/$Cfm::osv"; # Cfm os-ver dir
my $rc;                                 # Return code for system calls
my $RE_cmnt = "/^#|^$/";                # RE for comment lines
my @rpms;                               # List of rpm args
my $rpmfixesfn = "$admdir/rpmfixes";    # Fixup info for bad rpms
my $set;                                # Selected set name
my @sets;                               # List of set args
my %set_dat;                            # <setname><count><[file_idxs]>
my $statdir = "$Cfm::cfmroot/status";   # CFm status dir

my $usage = "
$Cfm::cfm_ver_rel;
Copyright (C) 1998 - 2000 Derald Metzger
This may be freely redistributed under the terms of the GNU GPL.
usage:
  cfmdb {--initdb}

  cfmdb {--init -i} {--host -h} <host>
  cfmdb {--init -i} {--rpm -r} [--all -a] [<rpm> ...]
  cfmdb {--init -i} {--set -s} <set> {<file> | <symlink>} [...]

  cfmdb {--hostset -h} <host> [...] [--add -a] [--delete -d] [--da] [<set> ...]

  cfmdb {--query -q} [--counts -c] [--all -a]
                     [--host -h] [<host> ...]
                     [--list -l]
                     [--set -s] [<set> ...]
                     [--file -f] [<file> ...]

  cfmdb {--erase -e} [--all -a] [--host -h] [<host> ...]
  cfmdb {--erase -e} [--all -a] [{--set -s} <set> ...] 
                                [[--file -f] <file> ...]
                                [-F <file> ...] [-L <symlink> ...]

  cfmdb {--version}
  cfmdb {--verbosity -v} <level>
";

Getopt::Long::Configure("bundling", "permute");
GetOptions(
           "a|all|add",
           "counts|c",
           "d|delete",
           "da",
           "erase|e",
           "file|f",
           "host|hostset|h",
           "initdb",
           "init|i",
           "list|l",
		   "nefile|F",
		   "neslnk|L",
           "query|q",
           "rpm|r",
           "set|s",
           "slnk",
           "verbosity|v=i", \$Cfm::vb_lvl,
           "version",
           "<>", \&get_args,
           )
    || die "### Invalid option! $usage";

#  Get possible residue args
@ARGV && do { get_args(shift @ARGV) };

#  Debug
($Cfm::vb_lvl > 3) && do {
    &print_opts;
    &print_arrays;
};

#  Must have a major mode
$opt_initdb|$opt_init|$opt_init|$opt_hostset|$opt_query|$opt_erase|$opt_version
    || die "### Invalid syntax. No major mode $usage";

#  Create a new cfmdb
$opt_initdb && do {
    $opt_host|$opt_rpm|$opt_set|@hosts|@sets && die "### Invalid syntax";
    -d $Cfm::cfmroot || die
        "### \$CFMROOT dir $Cfm::cfmroot does not exist.  Please create it.\n",
        "    Suggested location: /cm/cfm.  Exit";
    -d $admdir
        || mkdir("$admdir", 0755) || die "### Failed to create dir $admdir";
    -d $osvdir ||
        mkdir("$osvdir", 0755) || die "### Failed to create dir $osvdir";
    -d $statdir ||
        mkdir("$statdir", 0755) || die "### Failed to create dir $statdir";
	for my $file (@admfiles) {
		copy_file("/etc/cfm/$file", "$admdir/$file");
	}
    #  Init the all set
	print "## Ready to init the `all' set listed in $allfilesfn",
	"   This set should include files present on all hosts",
    "   which have not been included in some rpm. This will create",
    "   the `all' set entries, it will not alter existing entries.",
	"   Init of non-existant files will be skipped.\n",
    "   Proceed with init of `all' set [y|n](y)? ";
	<STDIN> =~ /^(n|N)$/ || do {
		file2arry($allfilesfn,my $cmnt=0,my $wslt=3,my $chop=0,\@ARGV)
			|| die "### Exit";
        Cfm::vt_nodes(\@ARGV, my $type=0, \$err_cnt, \@nodes);
		for $nd (@nodes) { init_dbf($nd, "all") }
	};
	die "##  cfmdb initialized at $Cfm::cfmroot\n";
};

#  Init mode options
$opt_init && do {
    @hosts && do {  # Create new host entry
        my $idx = read_hostsets(my $cmnts = 1);  # $idx is next open index

        ($host) = @hosts;
        $host =~ /^[a-z]+[a-z0-9-.]*$/o ||
			warn "### Bad hostname syntax: $host";
		if (chop (my ($line) = grep /^$host /, @hostsets)) {  # Exists
			($Cfm::vb_lvl > 2) && warn "## Entry exists for host:\n",
			"   $line @Cfm::def_sets\n";
		} else {  # Make new host entry
			$hostsets[$idx] = "$host ref $host-pnp $host";
			print "## $hostsets[$idx] @Cfm::def_sets\n";
			$hostsets[$idx] = "$hostsets[$idx]\n";
			write_hostsets();
		}
        #  Init the host specific sets
        # Get configured host specific files
		file2arry($hsfilesfn,my $cmnt=0,my $wslt=3,my $chop=0,\@ARGV)
			|| die "### Exit";  # Host specific files list
        for $idx (0,1,2,3,4,5,6) {  # Sv runcmd symlinks
            find(\&rcslwanted, "/etc/rc.d/rc$idx.d");
            sub rcslwanted { -l && push @ARGV, "$File::Find::name";	}
        }
        #  Validate and type the node args
        Cfm::vt_nodes(\@ARGV, my $type=0, \$err_cnt, \@nodes);
        for $nd (@nodes) {  # Init host specific files for this host
            init_dbf($nd, $host);  # Init dbf
        }
        exit;
    };
    (@rpms || ($opt_a && $opt_rpm)) && do {  # Create/add rpms' cfg files
        $opt_a && (@rpms = split ' ', `rpm -qa --queryformat '%{NAME} '`);
		my @fixes;  # Add/rm lines for rpm cfg files. Miss cfg fixes.
        file2arry($rpmfixesfn,my $cmnt=0, my $wslt=3, my $chop=0, \@fixes);
        for my $rpm (@rpms) {
	        my $file;  # Add/rm filename
            my $rpm1 = $rpm;
	        $rpm1 =~ s/\+/\\+/;
	        @nodes = @ARGV = ();
            @ARGV = split '\n', `rpm -qc $rpm`;
            for (@fixes) {
				(($file) = /$rpm1:add\s+(\S+)/) && push @ARGV, $file;
			}
            $ARGV[0] =~ /^(\(contains no files\)|$)/ && next;
            Cfm::vt_nodes(\@ARGV, my $type=0, \$err_cnt, \@nodes);
            for $nd (@nodes) { init_dbf($nd, $rpm); }  # Init rpm's cfg files
	        @nodes = @ARGV = ();
            for (@fixes) {
				(($file) = /$rpm1:rm\s+(\S+)/) && push @ARGV, $file;
			}
            Cfm::vt_nodes(\@ARGV, my $type=0, \$err_cnt, \@nodes);
		    for $nd (@nodes) { unlink "$Cfm::cfmroot$$nd[0]$$nd[1]$$nd[2]"; }
		}			   
		exit
    };
    @sets && do {  # Create or add files to a set
        @nodes || die "### No files to add to set";
        for $nd (@nodes) {  # Init the host specific files for the host
            ($set) = @sets;
            $fil_cnt++;
            init_dbf($nd, $set);  # Init dbf
        }
        exit;
    };

};

#  Hostset mode options
$opt_hostset && do {
    my $hidx;  # Current host's index in @hostsets
    my $idx = read_hostsets(my $cmnts = 1);  # $idx is next unused index
    for my $host (@hosts) {
        for($hidx = $idx; $hidx-- > 0; ) {  # Find this host's entry
            $hostsets[$hidx] =~ /^$host / && last;
        }
        if($hidx < 0) { die "### No host $host entry in hostsets\n"; }
        else {  # Make a new set list from the old
            my @newsets = split ' ', $hostsets[$hidx];
            shift @newsets;  # dump key
            ($opt_da || ($opt_a && $opt_d)) && do { # Replace old sets with new
                @newsets = ("ref", "$host-pnp", $host); # Preserve mandatory
                $opt_d = 0;  # Deleted the optional 
                $opt_a = 1;  # Use $opt_a to add the rest
            };
            $opt_d && do {  # Delete sets
              DELSETLP:
                for (@sets) {  # Use $_ for each arg set
                    /^(ref|$host-pnp|$host)$/ && do {
                        print "### Mandatory set $_ can NOT be deleted!\n";
                        next;
                    };
                    for my $newset (@newsets[3..$#newsets]) {  # Keep mandatory
                        /^$newset$/ && do {  # Matched
                            $newset = ();  # Erase set fm newsets
                            next DELSETLP;
                        };
                    }
                    print "### No set $_ found to delete for host $host\n";
                }
                $hostsets[$hidx] = join ' ', $host, @newsets;
                print "## $hostsets[$hidx] @Cfm::def_sets\n";
                $hostsets[$hidx] = "$hostsets[$hidx]\n";
                next;
            };
            $opt_a && do {
              ADDSETLP:
                for (@sets) {  # Use $_ for each arg set
                    /^[a-zA-Z]+[-+a-zA-Z0-9]*$/
                        || die "### Invalid setname syntax. Exit";
                    for my $newset (@newsets) {
                        /^$newset$/ && do {  # Matched
                            print "### Set $_ already exists on host $host\n";
                            next ADDSETLP;
                        };
                    }
                    push @newsets, $_;
                }
            };
            $hostsets[$hidx] = join ' ', $host, @newsets;
            print "$hostsets[$hidx] @Cfm::def_sets\n";
            $hostsets[$hidx] = "$hostsets[$hidx]\n";
        }
    }
    write_hostsets();
    exit;
};

#  Query mode options
$opt_query && do {
	$opt_counts|$opt_host|@hosts|$opt_set|@sets|$opt_file|@nodes ||
		die "### Invalid syntax $usage";
    $opt_counts|$opt_set|@sets|$opt_file|@nodes && &read_cfmdb();
    $opt_counts && do {
        my $set_cnt = keys %set_dat;
        my $rcs_cnt = keys %file_dat;
        print "## cfmdb counts -- sets: $set_cnt,  rcsfiles: $rcs_cnt,",
        "  file ver instances: $inst_cnt\n";
    };
    ($opt_host || @hosts) && do {  # Print requested hostset lines
        if(read_hostsets(my $cmnts = 0)) {  # Get entries without comments
            if($opt_a|$opt_host) {
                for my $hostset (@hostsets) {
                    chop $hostset;
                    print "$hostset @Cfm::def_sets\n";
                }
            }
            else {
                for my $host (@hosts) {
                    if(my ($line) = grep /^$host /, @hostsets) {
                        chop $line;
                        print "$line @Cfm::def_sets\n";
                    } else {
                        print "### No host $host entry in hostsets\n";
                    }
                }
            }
        } else { print "## No host entries in hostsets\n"; }
    };
    ($opt_set || @sets) && do {  # List sets & a count of their files
        ($opt_a || $opt_set) && (@sets = sort (keys %set_dat));
        for $set (@sets) {
            $set_dat{$set}[0] || ($set_dat{$set}[0] = 0);
            STDOUT->format_name("SET_CNT");
            write;  # Write set & its files count
            $opt_list && do {
                my $idx = 0;
                 while(defined($set_dat{$set}[1][$idx])) { # Print filenames
                    print "s:$set r:$set_dat{$set}[1][$idx++]\n";
                }
            };
        }
    };
    ($opt_file || @nodes) && do {  # List files & count of set memberships
        $opt_a && (@nodes = keys %file_dat);
        for $dbf (@nodes) {
            $opt_a || ($dbf = "$Cfm::cfmroot$$dbf[0]$$dbf[1]$$dbf[2]");
            print "$dbf:    $file_dat{$dbf}[0]\n";
            $opt_list && do {  # Print the set list
                print "  sets: ";
                for ( my $idx = 0; $idx < $file_dat{$dbf}[0]; $idx++) {
                    print " $file_dat{$dbf}[1][$idx]";
                }
                print "\n";
            };
        }
    };
    exit;
};

#  Erase mode options
$opt_erase && do {
    ($opt_host || @hosts) && do {
        read_hostsets(my $cmnts = 1);
        $opt_a && push @hosts, "all_hosts";
        for my $host (@hosts) {
            my $idx = 0;
            for my $line (@hostsets) {
                ($opt_a || ($line =~ /^$host /)) && do {
                    $line = ();
                    $opt_a || print "## Erased host $host\n";
                    $idx++;
                    $opt_a || last;
                };
            }
            $idx || print "### No entry for host $host in $hostsetsfn\n";
            $opt_a && print "## Deleted all hostset entries\n";
        }
        write_hostsets();
        exit;
    };
    # The fundamental operation for sets and files is removal of a set
    # from an rcsfile. If its the last set just remove the entire file.
    # Otherwise its a 2 step op: remove the set's branch, then its label.
    ($opt_set || @sets) && ($opt_file || @nodes) && $opt_a && die
        "### Invalid syntax: -a -s -{f|F|L}\n",
        "    Erase all of a set with -as, or all of a file with -af\n",
        "    It's ambiguous to specify -a with both -s and -f.\n";
    $opt_a && @sets && do {  # Erase all file memberships in these sets
        @sets || die "### No sets to erase";
        &read_cfmdb();  # Get set & file info from cfmdb.
        for $set (@sets) {  # Erase each set membership in each file
            while(my $fdx = $set_dat{$set}[0]--) {  # Get # of files in set
                $dbf = $set_dat{$set}[1][--$fdx];  # Do last file 1st
                if(--$file_dat{$dbf}[0]) {  # Not the last set in the dbf
                    Cfm::rm_setmem($dbf,$set) ||  # Erase set from dbf
                        warn "### Failed to erase set $set from $dbf";
                } else {  # Last set, remove $dbf
                    unlink $dbf;  # Erase the dbf
                }
            }
            print "## Erased set $set\n";
            exit;
        }
    };
    $opt_a && @nodes && do {  # Erase all sets in each file, ie delete files
        for $nd (@nodes) {
			$dbf = "$Cfm::cfmroot$$nd[0]$$nd[1]$$nd[2]";
            unlink $dbf;
			print "## Erased: $dbf\n";
        }
        exit;
    };
    @nodes && @sets && do {  # Erase some sets from some files
        &read_cfmdb();  # Get set & file info from cfmdb.
      NDLP:
        for $nd (@nodes) {  # Erase the sets from each file
            $dbf = "$Cfm::cfmroot$$nd[0]$$nd[1]$$nd[2]";
            ($Cfm::vb_lvl > 2) && print "## Erasing sets in $dbf\n";
            for $set (@sets) {  # Erase each set from dbf
                ($Cfm::vb_lvl > 2) && print "  $set";
                if(--$file_dat{$dbf}[0]) {  # Not last set
                    Cfm::rm_setmem($dbf,$set) ||  # Erase set from dbf
                        warn "## Failed to erase set $set from $dbf";
                } else {  # Zero, last set
                    unlink $dbf;  # Erase the dbf
                    ($Cfm::vb_lvl > 2) &&
                        print " - Last set. Deleted $dbf\n";
                    next NDLP;
                }
                print "\n";
            }
        }
        exit;
    };
    die "### Invalid syntax. Nothing to erase";
};

#  Version mode
$opt_version && do {
    die "## $Cfm::cfm_ver_rel\n";
};

#  Must have a major mode
die "### No major mode! $usage";

#####################  subroutines and formats  #####################
#----------
#  Read the hostsets file into @hostsets
# $cmnts => Include comments.
# returns next unused @hostsets index 
sub read_hostsets($cmnts) {
    my ($cmnts) = @_;
    open(HOSTS, "$hostsetsfn") 
                || die "### Can't open $hostsetsfn for read. Exit";
    @hostsets = <HOSTS>;
    close HOSTS;
    $cmnts || (@hostsets = grep !/^#|^$/, @hostsets);
    return $#hostsets + 1;
}

#----------
#  Write @hostsets array to cfm hostsets file
sub write_hostsets() {
    open(HOSTS, ">$hostsetsfn")
                || die "### Can't open $hostsetsfn for write. Exit";
    print HOSTS @hostsets;
    close HOSTS;
}

#----------
#  This routine collects args associated with a single option
# flag. It pushes the arg passed by GetOptions and any following args
# in the @ARGV array (some exceptions) onto an appropriate target
# array until the next option is encountered. The target array choice
# is based on which option flag has been set. The flag is then
# cleared. This routine will also set the hostset mode if the
# $opt_host control flag is encountered before any other mode has been
# set.
# Modes stay set, flags get cleared and pickup associated args.
#   modes: initdb, init, hostset, query, erase, version
#   flags: host, rpm, set, file

sub get_args($arg) {
    my ($arg) = @_;
    my $ref;

    unshift @ARGV, $arg;  # Put back 1st arg
  GA_SW: {
      $opt_init && do {  # Check legal things for init mode
          $opt_host && do {  # Flagged host for init
              $opt_host = 0;
              @hosts && die "### Invalid syntax. Only one host for init";
              $_ = shift @ARGV;
              ($_ eq '.') && ($_ = $hnam);  # `.' = the executing host's name
              push @hosts, $_;
              return;
          };
          $opt_rpm && do {  # Flagged rpms for init
              $opt_rpm = 0;
              $ref = \@rpms;
              last;
          };
          $opt_set && do {  # Flagged set for init
              $opt_set = 0;
              @sets && die "### Invalid syntax. Only one set for init";
              $_ = shift @ARGV;
              ($_ eq '.') && ($_ = $hnam);  # `.' = the executing host's name
              push @sets, $_;
              return;
          };
          # Unflagged, must be files for init set. test'em, push'em on @nodes
          Cfm::vt_nodes(\@ARGV, my $type=0, \$err_cnt, \@nodes);
          return;
      };
    $opt_host && do {  # Flagged host args. init was trapped above
        #  If not query or erase mode before a host arg then make mode hostset
        $opt_host = 0;
        $opt_query || $opt_erase || ($opt_hostset = 1);
        $ref = \@hosts;
        last;
    };
    $opt_set && ($opt_query || $opt_erase) && do {
        $opt_set = 0;
        $ref = \@sets;
        last;
    };
    $opt_file && ($opt_query || $opt_erase) && do {
        $opt_file = 0;
        Cfm::vt_nodes(\@ARGV, my $type=0, \$err_cnt, \@nodes);
        return;
    };
    $opt_nefile && $opt_erase && do {  # Non-existant files to erase in
		$opt_nefile = 0;
	    Cfm::vt_nodes(\@ARGV, my $type=",v", \$err_cnt, \@nodes);
        return;
	};
    $opt_neslnk && $opt_erase && do {  # Non-existant symlinks to erase in
		$opt_neslnk = 0;
	    Cfm::vt_nodes(\@ARGV, my $type=".cfmsl,v", \$err_cnt, \@nodes);
        return;
	};
    $opt_hostset && do {  # Unflagged must be sets for hostsets
        $ref = \@sets;
        last;
    };
    die "### Invalid cmd syntax! $usage";
  }

    #  The array is idd. Push the args on it
    while(($_ = $ARGV[0]) && !/^-/) {
        shift @ARGV;
        ($_ eq '.') && ($_ = $hnam);  # `.' => executing host's name
        push @$ref, $_;
    }
}

#----------
# This routine copies a file.
# It prompts the user before overwrite an existing file.

sub copy_file($srcf,$fn) {
    my ($srcf, $fn) = @_;
    !(-f $fn) && do {
        copy("$srcf","$fn") || die "### Failed: copy($srcf, $fn). Exit";
		return;
    };
	print "## Diffing existing $fn\n";
	if(system("diff", @Cfm::diffopts, "-s", $srcf, $fn)) {  # Files differ
		print "   Overwrite $fn? [y|n](n) ";
		<STDIN> =~ /^(y|Y)$/ && do {
			copy("$srcf","$fn") || die "### Failed: copy($srcf, $fn). Exit";
			return;
		};
		print "## Did NOT change existing $fn\n";
	}
}

#----------
# This routine initializes a cfmdb rcsfile.
# Ie, it creates the cfmdb dir and file as rqd,
# then it uploads the executing hosts copy of the file.

sub init_dbf($ $) {
    my ($nd, $set) = @_;
    my $brch;         # Next available branch in $dbf
    my $dbf = "$Cfm::cfmroot$$nd[0]$$nd[1]$$nd[2]";
    my $fn = $$nd[1]; # Cannonical filename
    my $found_set;    # Discarded
    my $lvl;          # Version level
    my $msg = '-m"cfmdb init"';
    
    my $sl_flg = $$nd[2] =~ /^(.cfmsl),v/;  # Symlink flag
    my $hfn = "$fn$1";         # Host side file to rcsdiff
    
    # Verify access
    -e $fn || do {
        warn "### Skip! No file: $fn\n";
        $err_cnt++; return 0;
    };
    chdir dirname($fn) || do {
        warn "### Skipping! No dir access for $fn\n";
        $err_cnt++; next;
    };
    system('df -k . | grep : >/dev/null') || do {
        warn "### Skipping! NFS mounted $fn\n";
        $err_cnt++; next;
    };
    # Id and make the symlink info file as rqd
    $sl_flg && do {  # Make text file for symlink
        open(CFMSL,">$hfn") || die "\### Can't open $hfn for symlink";
        print CFMSL "$fn -> ", readlink "$fn", "\n";
        close CFMSL;
    };
    # Get the branch number
    if(-f $dbf) {  # Existing rcsfile. Get set tag or next available branch
        if($lvl = Cfm::chk_rev($dbf, \$found_set, \$brch, $set)) { # Found it
			($Cfm::vb_lvl >2) && 
				warn "## NO init! set `$set' exists: $dbf\n";
			return 0;
		}
    } else  {  # No file so make it
        if(!-d dirname($dbf)) {  # Make dir as rqd
            mkpath(dirname($dbf), 0, 0755) || do {
				warn "### Can't make cfmdb dir for $dbf";
				$err_cnt++; return 0;
			};
		}
        @args=("ci","-q","-u","-f","-N$set","-t-cfmdb_rcsfile","$msg","$dbf");
        ($rc = 0xffff & system(@args)) && do {
            ($Cfm::vb_lvl > 3) && bad_syscall($rc,@args);
            warn "### Init failed: s:$set, r:$dbf";
            $err_cnt++; return 0;
        };
        @args=("rcs", "-q","-U", "-ko", "$dbf");
        ($rc = 0xffff & system(@args)) && do {
            ($Cfm::vb_lvl > 3) && bad_syscall($rc,@args);
            warn "### Init failed: s:$set, r:$dbf";
            $err_cnt++; return 0;
        };
        $brch = "1.1.1";
    }
    # Checkin the file
    @args=("ci", "-q", "-u", "-f","-r$brch.1","-N$set","$msg","$dbf");
    ($rc = 0xffff & system(@args)) && do {
        ($Cfm::vb_lvl > 3) && bad_syscall($rc,@args);
        die "### Init failed: s:$set, r:$dbf";
        $err_cnt++; return 0;
    };
    # Cleanup as rqd
    $sl_flg && unlink "$hfn";
}


#----------
# This routine parses all rcsfiles in the cfmdb.
# It collects set (symbol) info in %set_dat and file info in %file_dat.
#   %set_dat entry:  {setname}->[<$file_count>,[<@rcsfiles>]]
#   %file_dat entry: {rcsfile}->[$set_count>,[<@sets>]]
# It updates global vars $inst_count.
# You can list the sets, a count of their rcsfiles, and the rcsfiles.
# You can list the rcsfiles, a count of their sets, and the sets.
# Other globals: $err_cnt.

sub read_cfmdb() {
    my ($dbf, $rsym_flg, $set);
    %file_dat = ();
    %set_dat = ();

    find(\&wanted, "$Cfm::cfmroot/");
    sub wanted {  # Do all rcsfiles. Ignore others
        /.*,v/ && do {
            open(RCSFILE, $dbf = "$File::Find::name") || do {
                print "\### Can't open RCSFILE $dbf";
                $err_cnt++;
                return 0;
            };
            $rsym_flg = 0;
            while(<RCSFILE>) {
                $rsym_flg || /^symbols[;]?$/ || next;
                $rsym_flg || /^symbols[;]?$/ && ($rsym_flg = 1) && next;
                /^locks[;]?/ && last;
                $inst_cnt++;
                ($set) = $_ =~ /\t([-\w+]+):.*/;
                $file_dat{$dbf}[0]++;
                push @{ $file_dat{$dbf}[1] }, $set; # File is a member of set
                $set_dat{$set}[0]++;
                push @{ $set_dat{$set}[1] }, $dbf;  # Set includes file
            }
        };
    }
close(RCSFILE);
}

format SET_CNT =
@<@<<<<<<<<<<<<<<  @<<<<<<<<<
"s:", $set,          $set_dat{$set}[0]
.

#----------
sub print_opts() {  # Print for debug 
    print "####  Dump of opts\n",
    "  \@ARGV= ", join(' ', @ARGV), "\n",
    "  Flags: host|hostset|h=$opt_host rpm|r=$opt_rpm set|s=$opt_set file|f=$opt_file F=$opt_nefile L=$opt_neslnk\n",
    "  Mode: initdb=$opt_initdb init=$opt_init hostset=$opt_hostset query=$opt_query erase=$opt_erase version=$opt_version\n",
    "  a|all|add=$opt_a     d=$opt_d    da=$opt_da   list|l=$opt_list   counts|c=$opt_counts",
    "  \$Cfm::vb_lvl=$Cfm::vb_lvl\n";
}

#----------
sub print_arrays() {  # Print arg arrays
    print "####  Dump of arg arrays\n",
    "  \@hosts    = ", join(' ', @hosts), "\n",
    "  \@rpms     = ", join(' ', @rpms), "\n",
    "  \@sets     = ", join(' ', @sets), "\n",
    "--\@nodes:\n";
    for (@nodes) { print "  ", join ' : ', @$_, "\n"; }
}

#----------
sub bad_syscall($ @) {
    my ($rc, @args) = @_;
    warn "### Failed: system(@args)";
    printf "\n    returned: %#04x\n", $rc;
}

#----------
# This routine reads a file containing a list of items,
# one per line. It pushes the filtered output onto an array.
# It can strip the comments, leading and trailing whitespace,
# and the trailing linefeed.
#ARGS:
#   $cmnt = 0 : Remove comments, full line and trailing. Also empty lines.
#   $wslt = 0 : Leave leading and trailing whitespace alone.
#         = 1 : Remove leading whitespace.
#         = 2 : Remove trailing whitespace, including linefeed.
#         = 3 : Remove leading & trailing whitespace.
#   $chop = 0 : Leave the linefeed alone.
#           1 : Remove the trailing linefeed.
# Comments are assumed to start with `#' in the 1st column.
# Embedded white space is conditioanlly permitted within the item.
#OUTPUT: 
# Entries that make it thru the filter are pushed on the
# array passed by ref.

sub file2arry($ $ $ $ $) {
	my ($fn, $cmnt, $wslt, $chop, $a_ref) = @_;
    open(F2A, $fn) || do { warn "### Failed: open $fn\n"; return 0; };
	while(<F2A>) {
		$cmnt || do {
			m/^#|^\s*$/o && next;  # Strip full line cmnts & empty lines
			(($_) = /(^[^#]+).*/) || next;  # Strip trailing cmnts
		};
	    $wslt && do {
			($wslt == 1) && do { (($_) = /^\s*(\S+(\s+\S+)*\s*$)/o) || next; };
			($wslt == 2) && do { (($_) = /^(\s*\S+(\s+\S+)*)\s*$/o) || next; };
		    ($wslt == 3) && do { (($_) = /^\s*(\S+(\s+\S+)*)\s*$/o) || next; };
		};
		$chop && chop $_;
	    push @$a_ref, $_;
	}
	close LFILE;
	return 1
}

__END__
