# @(#)92    1.6  src/bos/usr/bin/cdat/cdat.pm, cdat, bos720 7/14/11 17:56:29
# IBM_PROLOG_BEGIN_TAG 
# This is an automatically generated prolog. 
#  
# bos720 src/bos/usr/bin/cdat/cdat.pm 1.6 
#  
# Licensed Materials - Property of IBM 
#  
# COPYRIGHT International Business Machines Corp. 2010,2011 
# All Rights Reserved 
#  
# US Government Users Restricted Rights - Use, duplication or 
# disclosure restricted by GSA ADP Schedule Contract with IBM Corp. 
#  
# IBM_PROLOG_END_TAG 
package cdat;
require Exporter;

@ISA = qw(Exporter);
@EXPORT = qw(catgets DEBUG);

use strict;
use warnings;
use POSIX;		# needed for isatty, Termios
use Fcntl ':mode';	# for S_IRWXG/S_IRWXO
use Net::Ping;
use Net::FTP;
use MIME::Base64;	# for encode_base64
use XML::LibXML;
use messages;

#
# Globals.
#
# Retrieve the value of the CDAT_DEBUG environment variable.
my $debug = $ENV{CDAT_DEBUG};

#
# Constants.
#
my $ODMGET    = 'ODMDIR=/etc/objrepos /usr/bin/odmget';
my $ODMDEL    = 'ODMDIR=/etc/objrepos /usr/bin/odmdelete';
my $ODMADD    = 'ODMDIR=/etc/objrepos /usr/bin/odmadd';
my $ODMCHG    = 'ODMDIR=/etc/objrepos /usr/bin/odmchange';
my $LOCALE    = '/usr/bin/locale';
my $EXPECT    = '/usr/bin/expect';
my $SSH       = '/usr/bin/ssh';
my $EXPECT_TELNET = '/usr/lib/cdat/telnet.exp';
my @CDAT_SCRIPTS = ('/usr/lib/cdat/types', '/var/adm/ras/cdat');
my $MANIFEST  = 'manifest.xml';
my $DEF_PATH  = '/cdat';
my $DEF_USER  = 'cdat';
my $HOST_DB   = ".cdatrc";
# localization
my $DSPCAT    = '/usr/bin/dspcat';
my $CATALOG   = 'cdat.cat';

my $PING_TIMEOUT = 2;

our $REMOTE_AUTO        = 0;
our $REMOTE_DONTASKUSER = 1;
our $REMOTE_ASKUSER     = 2;
my  $REMOTE_UIMASK	= 3;
our $REMOTE_PROBE       = 4;

######################################################################
# Function:	cdat::catgets
# Purpose:	Return the specified message from the message catalog.
# Tasks:	Call /usr/bin/dspcat to retrieve the message.
# Input:	Message number, default message
# Output:	Message from the message catalog or default message
#		if not found
######################################################################
sub catgets
{
    my ($id, $default) = @_;
    my $str;

    $str= `$DSPCAT $CATALOG 1 $id 2>/dev/null`;
    $str = $default if ($? != 0);
    return $str;
}

######################################################################
# Function:	cdat::yes_no
# Purpose:	Ask a question (yes or no) to the user.
# Tasks:	Display a prompt and read an answer from standard input.
# Input:	Default answer (1=yes, 0=no)
# Output:	1 if user answers [yY]
#		0 if user answers [nN]
#		Default answer otherwise
######################################################################
sub yes_no
{
    my ($rc) = @_;

    if ($rc) {
        # default answer is "yes"
        printf(catgets(MSG_YES_NO_YES, " (y/n) [y]: "));
    } else {
        # default answer is "no"
        printf(catgets(MSG_YES_NO_NO,  " (y/n) [n]: "));
    }
    my $choice = <STDIN>;
    chomp($choice);

    my $yes = catgets(MSG_YES, "y");
    my $no  = catgets(MSG_NO,  "n");
    $choice = lc($choice);

    if ($choice eq $yes) {
        $rc = 1;
    } elsif ($choice eq $no) {
        $rc = 0;
    }
    return $rc;
}

######################################################################
# Function:	cdat::get_password
# Purpose:	Read a password from the terminal.
# Tasks:	Disable echo from the terminal, read password and
#		re-enable echo.
# Input:	Prompt asking for a password
# Output:	Password or undef if standard input is not a TTY
######################################################################
sub get_password
{
    my ($prompt) = @_;

    # Check that standard input is a tty
    return undef unless (isatty(STDIN_FILENO));

    my $termios = POSIX::Termios->new();
    return undef unless ($termios->getattr(STDIN_FILENO));
    my $lflag = $termios->getlflag();

    # Disable echoing
    $termios->setlflag($lflag & ~ECHO);
    return undef unless ($termios->setattr(STDIN_FILENO, TCSANOW));

    print("$prompt");
    my $passwd = <STDIN>;
    print("\n");

    # Restore terminal parameters
    $termios->setlflag($lflag);
    $termios->setattr(STDIN_FILENO, TCSANOW);

    chomp($passwd);
    return $passwd;
}

######################################################################
# Function:	cdat::define_password
# Purpose:	Ask user to chose a password and to confirm it.
# Tasks:	Read the password two times.
# Input:	Prompt asking for a password and prompt asking to
#		confirm the password
# Output:	Password or undef if standard input is not a TTY
#		or if passwords don't match
######################################################################
sub define_password
{
    my ($prompt1, $prompt2) = @_;

    # Ask for password a first time
    my $passwd1 = get_password($prompt1);
    return undef unless (defined($passwd1));

    # Ask to confirm password
    my $passwd2 = get_password($prompt2);
    return undef unless (defined($passwd2));

    # Check that both inputs match
    return undef unless ($passwd1 eq $passwd2);

    return $passwd1;
}

######################################################################
# Function:     do_telnet
# Purpose:      Execute a command on a remote node using telnet.
# Tasks:        telnet to the remote node and automatically enter
#               user name, password and command using expect.
# Input:        user name, host name, node type, command to execute
# Output:       None
######################################################################
sub do_telnet
{
    my ($hosts, $user, $host, $cmd, $mode) = @_;

    $mode &= $REMOTE_UIMASK;

    my $passwd = $hosts->{$host}{users}{$user};
    if ($mode != $REMOTE_DONTASKUSER &&
        (!defined($passwd) || $mode == $REMOTE_ASKUSER)) {
	# Ask for password now; it is needed for both FTP (to retrieve
	# login and password prompts) and telnet.
	my $prompt = sprintf(catgets(MSG_ENTER_PASSWD,
	    "Enter %s@%s\'s password: "), $user, $host);
	$passwd = cdat::get_password($prompt);
    }
    if (!defined($passwd)) {
        $? = 1;
        return undef;
    }

    # Check if we have login and password prompts in host database.
    my $login_pattern = $hosts->{$host}{login_pattern};
    my $password_pattern = $hosts->{$host}{password_pattern};
    if (!defined($login_pattern) || !defined($password_pattern)) {
        # Either login or password prompt is unknown, try to retrieve
	# both.  If it fails, we will get the default (C locale) values.
	($login_pattern, $password_pattern) =
	    cdat::get_telnet_patterns($host, $user, $passwd);
    }
    # Pass parameters to expect through environment variables.
    $ENV{EXPECT_PATTERN1} = $login_pattern;
    $ENV{EXPECT_PATTERN2} = $password_pattern;
    $ENV{EXPECT_PASSWORD} = $passwd;

    my @lines = `$EXPECT_TELNET '$user' '$host' '$cmd' 2>/dev/null`;
    $? = WEXITSTATUS($?);

    shift @lines;	# remove echo'ed command
    pop @lines;		# remove last prompt
    # telnet uses DOS-style EOL CR-LF, so convert to UNIX
    foreach (@lines) { s/\r$//; }

    return @lines;
}
######################################################################
# Function:     do_ssh
# Purpose:      Execute a command on a remote node using ssh.
# Tasks:        ssh to the remote node and execute command.
# Input:        user name, host name, and command to execute
# Output:       None
######################################################################
sub do_ssh
{
    my ($hosts, $user, $host, $cmd, $mode) = @_;
    my @lines;

    $mode &= $REMOTE_UIMASK;
    if ($mode == $REMOTE_AUTO) {
        @lines = `$SSH -i $ENV{HOME}/.ssh/id_rsa -o UserKnownHostsFile=$ENV{HOME}/.ssh/known_hosts $user\@$host '$cmd'`;
    } elsif ($mode == $REMOTE_DONTASKUSER) {
        @lines = `$SSH -i $ENV{HOME}/.ssh/id_rsa -o UserKnownHostsFile=$ENV{HOME}/.ssh/known_hosts -o StrictHostKeyChecking=yes -o PasswordAuthentication=no -o NumberOfPasswordPrompts=0 $user\@$host '$cmd'`;
    } elsif ($mode == $REMOTE_ASKUSER) {
        @lines = `$SSH -i $ENV{HOME}/.ssh/id_rsa -o UserKnownHostsFile=$ENV{HOME}/.ssh/known_hosts -o PubkeyAuthentication=no $user\@$host '$cmd'`;
    }
    $? = WEXITSTATUS($?);
    return @lines;
}

######################################################################
# Function:     rexec
# Purpose:      implements rexec (port 512) protocol
# Tasks:        send a command to a remote host using rexec protocol
# Input:        user name, password, host name, and command to execute
# Output:       result of the command
######################################################################
sub rexec
{
    my ($user, $passwd, $host, $cmd) = @_;
    my ($result, $output);

    my $sock = IO::Socket::INET->new(PeerAddr => $host,
	PeerPort => 'exec(512)', Proto => 'tcp');
    $sock->syswrite("0\0", 2);
    $sock->syswrite($user."\0", length($user) + 1);
    $sock->syswrite($passwd."\0", length($passwd) + 1);
    $sock->syswrite($cmd."\0", length($cmd) + 1);

    $result = $sock->sysread($output, 1);
    if ($output ne chr(0)) {
        $? = 1;
    } else {
        $? = 0;
    }
    return $sock->getlines;
}

######################################################################
# Function:     do_rexec
# Purpose:      execute a command on a remote host using rexec protocol
# Tasks:        send a command to a remote host using rexec protocol
#               read password from DB and retrieve the exit code number
# Input:        user name, host name, and command to execute
# Output:       result of the command and exit code
######################################################################
sub do_rexec
{
    my ($hosts, $user, $host, $cmd, $mode) = @_;

    $mode &= $REMOTE_UIMASK;

    my $passwd = $hosts->{$host}{users}{$user};
    if ($mode != $REMOTE_DONTASKUSER &&
        (!defined($passwd) || $mode == $REMOTE_ASKUSER)) {
        my $prompt = sprintf(catgets(MSG_ENTER_PASSWD,
	    "Enter %s@%s\'s password: "), $user, $host);
        $passwd = get_password($prompt);
    }
    if (!defined($passwd)) {
        $? = 1;
        return undef;
    }
    my @lines = rexec($user, $passwd, $host, " ( ".$cmd." ) ; echo \$?");
    if ($? == 0) {
        # exit status is printed in the last line
        $? = pop(@lines);
    }
    return @lines;
}

######################################################################
# Function:	cdat::remote_cmd
# Purpose:	Execute a shell command on a remote node.
# Tasks:	Execute the specified command on the remote node.
# Input:	Node (user, host), command to execute and flags
# Output:	Status code returned by the remote command
######################################################################
sub remote_cmd
{
    my ($user, $host, $cmd, $mode) = @_;
    my $rc = 0;
    my @lines;
    my %hosts;
    my $protocol;

    read_host_db(\%hosts);

    $protocol = $hosts{$host}{protocol};
    if (!defined($protocol) || ($mode & $REMOTE_PROBE)) {
        if ( -x $SSH && cdat::check_remote_protocol($host, 'ssh')) {
            $protocol = "ssh";
        } elsif (cdat::check_remote_protocol($host, 'exec')) {
            $protocol = "exec";
        } elsif (cdat::check_remote_protocol($host, 'telnet')) {
            $protocol = "telnet";
        } else {
            return undef;
        }
    }

    if ( -x $SSH && $protocol eq 'ssh') {
        @lines = do_ssh(\%hosts, $user, $host, $cmd, $mode);
        return @lines;
    } elsif ($protocol eq 'exec') {
        @lines =  do_rexec(\%hosts, $user, $host, $cmd, $mode);
        return @lines;
    } elsif ($protocol eq 'telnet') {
        @lines = do_telnet(\%hosts, $user, $host, $cmd, $mode);
        return @lines;
    } else {
        return undef;
    }
}

######################################################################
# Function:	cdat::get_node_info
# Purpose:	Parse raw node (in the form "type:user@name").
# Tasks:	Split components of raw node string (type, user and
#		name) and return them.
# Input:	Raw node string
# Output:	Node name, type and user name (or undef)
######################################################################
sub get_node_info
{
    my ($item) = @_;
    my %info;

    my ($type, $node) = split(/:/, $item, 2);

    if ($type ne 'HMC'  && $type ne 'VIOS' &&
        $type ne 'LPAR' && $type ne 'PSCALE') {
        return undef;
    }

    my ($user, $name) = split(/@/, $node, 2);
    if (!defined($name)) {
        $name = $user;
        $user = undef;
    }
    $info{user} = $user;
    $info{type} = $type;

    return ($name, %info);
}

######################################################################
# Function:	cdat::read_nodes_from_array
# Purpose:	Parse raw nodes (in the form "type:user@name").
# Tasks:	Split components of raw nodes (type, user and name)
#		and store them into a hash of nodes.
# Input:	Array of raw nodes
# Output:	Hash of nodes
######################################################################
sub read_nodes_from_array
{
    my (@rawnodes) = @_;
    my %nodes;

    # Each entry in the array is of the form:
    # <type>:[<user>@]<node>

    foreach my $rawnode (@rawnodes) {
        my ($type, $node) = split(/:/, $rawnode, 2);
	if (!defined($node) || length($type) == 0 || length($node) == 0) {
	    printf(STDERR catgets(MSG_SKIPPING_INVALID_NODE,
	    	"Skipping invalid node %s.\n"), $rawnode);
	    next;
	}
	my ($user, $name) = split(/@/, $node, 2);
	if (!defined($name)) {
	    # no 'user@' part found
	    $nodes{$user}{type} = $type;
	} else {
	    $nodes{$name}{type} = $type;
	    $nodes{$name}{user} = $user;
	}
    }
    return %nodes;
}

######################################################################
# Function:	cdat::read_nodes_from_files
# Purpose:	Parse a set of nodes.txt files.
# Tasks:	For each file, parse each line of the file and build
#		a hash of nodes.
# Input:	Array of filenames
# Output:	Hash of nodes or undef
######################################################################
sub read_nodes_from_files
{
    my (@filenames) = @_;
    my %nodes;
    my $rc;

    foreach (@filenames) {
        # Each line of the file is of the form:
        # <type>:[<user>@]<node>

        $rc = open(IN, $_);
        if (!$rc) {
            printf(STDERR catgets(MSG_CANNOT_OPEN_FILE,
	        "Cannot open file %s.\n"), $_);
	    next;	# skip this file
        }
        while (<IN>) {
            next if /^\s*#/;	# skip single-line comments from file
	    # ignore trailing comments
	    s/#.*$//;
	    s/\s+$//;
            chomp;
            my ($name, %info) = get_node_info($_);

            $nodes{$name}{type} = $info{type};
            $nodes{$name}{user} = $info{user};
        }
        close(IN);
    }
    return %nodes;
}

######################################################################
# Function:	cdat::collect_node_info
# Purpose:	Retrieve information from a node.
# Tasks:	Connect to the node and retrieve the machine id,
#		the LPAR id (if any) and the timezone.
# Input:	Node (type, user and name)
# Output:	Information about the node (machine id, LPAR id, TZ)
######################################################################
sub collect_node_info
{
    my ($type, $user, $name) = @_;
    my %info;

    if ($type eq 'HMC' || $type eq 'LPAR' || $type eq 'VIOS') {
        my @tz = remote_cmd($user, $name, 'date +%Z', $cdat::REMOTE_DONTASKUSER);
        return %info if ($? != 0 || !defined($tz[0]));
        chomp($tz[0]);
        $info{tz} = $tz[0];
    } else {
        # this is for nodes of type PSCALE for instance
        $info{tz} = 'unknown';
    }
    if ($type eq 'LPAR' || $type eq 'VIOS') {
        my @uname = remote_cmd($user, $name,
	    'uname -mL', $cdat::REMOTE_DONTASKUSER);
	return %info if ($? != 0 || !defined($uname[0]));
	($info{machine_id}, $info{lpar_id}) = split(/\W/, $uname[0]);
    }
    return %info;
}

######################################################################
# Function:	cdat::odm_set_user
# Purpose:	Store username inside ODM.
# Tasks:	Store the username in the SWServAt ODM database,
#		under the "cdat_user" attribute.
# Input:	Username
# Output:	None
######################################################################
sub odm_set_user
{
    my ($user) = @_;
    my %attributes;

    odm_del('SWservAt', 'attribute=cdat_user');

    $attributes{attribute} = 'cdat_user';
    $attributes{value} = "$user";
    $attributes{deflt} = "$DEF_USER";

    odm_add("SWservAt", %attributes);
}

######################################################################
# Function:	cdat::odm_get_user
# Purpose:	Retrieve username from ODM.
# Tasks:	Retrieve the username from the SWServAt ODM database,
#		that is stored under the "cdat_user" attribute.
# Input:	None
# Output:	Username or undef
######################################################################
sub odm_get_user
{
    my @list = odm_get('SWservAt', 'attribute=cdat_user');
    return undef if (!@list);

    my %item = %{$list[0]};

    return $item{value};
}

######################################################################
# Function:	cdat::odm_set_path
# Purpose:	Store path to repository inside ODM.
# Tasks:	Store the path to the repository in the SWServAt ODM
#		database, under the "cdat_directory" attribute.
# Input:	Path
# Output:	None
######################################################################
sub odm_set_path
{
    my ($path) = @_;
    my %attributes;

    odm_del('SWservAt', 'attribute=cdat_directory');

    $attributes{attribute} = 'cdat_directory';
    $attributes{value} = "$path";
    $attributes{deflt} = "$DEF_PATH";

    odm_add('SWservAt', %attributes);
}

######################################################################
# Function:	cdat::odm_get_path
# Purpose:	Retrieve path to repository from ODM.
# Tasks:	Retrieve the path to the repository from the SWServAt
#		ODM database, that is stored under the "cdat_directory"
#		attribute.
# Input:	None
# Output:	Path or undef
######################################################################
sub odm_get_path
{
    my @list = odm_get('SWservAt', 'attribute=cdat_directory');
    return undef if (!@list);

    my %item = %{$list[0]};

    # remove trailing "/" from path
    $item{value} =~ s/\/+$//;
    return $item{value};
}

######################################################################
# Function:	cdat::locale_charmap
# Purpose:	Retrieve the current character mapping.
# Tasks:	Retrieve the current character mapping.
# Input:	None
# Output:	Character mapping (e.g "ISO8859-1")
######################################################################
sub locale_charmap
{
    my $charmap = `$LOCALE charmap`;

    if (!defined($charmap) || $charmap eq '') {
        $charmap = "ISO8859-1";
    } else {
        chomp($charmap);
    }
    return $charmap;
}

######################################################################
# Function:	cdat::DEBUG
# Purpose:	Print diagnostic message.
# Tasks:	Print diagnostic message to stderr if the environment
#		variable CDAT_DEBUG is set and its value is greater
#		or equal to the specified debug level.
# Input:	debug level, diagnostic message
# Output:	None
######################################################################
sub DEBUG
{
    return unless defined($debug);
    my $level = shift;
    print(STDERR @_) if ($debug >= $level);
}

######################################################################
# Function:     cdat::check_remote_protocol
# Purpose:      Check remote system available service
# Tasks:        Check if remote system is running a given service
#               by testing if the service port is open
# Input:        remote hostname and protocol to test ("ssh", "ftp", ...)
# Output:       1 on success, 0 on error
######################################################################
sub check_remote_protocol
{
    my ($hostname, $protocol) = @_;
    my $rc;

    # probe the remote port

    my $p = Net::Ping->new('tcp', $PING_TIMEOUT);
    $p->{'port_num'} = getservbyname($protocol, 'tcp');
    $p->{'econnrefused'} = 1;
    $rc = $p->ping($hostname);
    $p->close();

    return $rc;
}

######################################################################
# Function:     cdat::get_telnet_patterns
# Purpose:      get from remote system login and password prompts
# Tasks:        Connect to a given remote system and, using ftp,
#               retrieve used language (from /etc/environment, LANG)
#               and extract login and password promptes from
#               /usr/lib/nls/msg/$LANG/tsm.cat
# Input:        remote hostname, user login and password
# Output:       an array with two entries, the login and password
#               patterns
######################################################################
sub get_telnet_patterns
{
    my ($hostname, $user, $passwd) = @_;
    my ($login_pattern, $password_pattern) = ("*ogin: ", "*assword:");
    my $cdat_environment = "/tmp/cdat_environment.$$";
    my $cdat_tsm_cat = "/tmp/cdat_tsm_cat.$$";
    my ($rc, $line);

    my $ftp = Net::FTP->new($hostname);
    if (!defined($ftp)) {
	return ($login_pattern, $password_pattern);
    }
    $rc = $ftp->login($user, $passwd);
    if (!$rc) {
	return ($login_pattern, $password_pattern);
    }
    if (!$ftp->cwd("/etc")) {
	$ftp->quit();
	return ($login_pattern, $password_pattern);
    }
    $rc = $ftp->get("environment", $cdat_environment);
    if (!$rc) {
        $ftp->quit();
	return ($login_pattern, $password_pattern);
    }
    open(ENVFILE, $cdat_environment);
    unlink($cdat_environment);
    my $LANG = "C";
    while (defined($line = <ENVFILE>)) {
        if ($line =~ m/^LANG=/) {
            $LANG = $line;
            $LANG =~ s/^LANG=//;
            chomp($LANG);
        }
    }
    close(ENVFILE);

    if ($LANG eq "C") {
	$ftp->quit();
	return ($login_pattern, $password_pattern);
    }
    $rc = $ftp->cwd("/usr/lib/nls/msg/$LANG");
    if (!$rc) {
	$ftp->quit();
	return ($login_pattern, $password_pattern);
    }
    $rc = $ftp->get("tsm.cat", $cdat_tsm_cat);
    if (!$rc) {
	$ftp->quit();
	return ($login_pattern, $password_pattern);
    }

    my @lines = `$DSPCAT $cdat_tsm_cat 1 50 2>/dev/null`;
    if ($? == 0) {
	$login_pattern = $lines[-1];
    }
    @lines = `$DSPCAT $cdat_tsm_cat 1 2 2>/dev/null`;
    if ($? == 0) {
	$password_pattern = $lines[-1];
	$password_pattern =~ s/%s/*/g;
    }

    unlink($cdat_tsm_cat);

    return ($login_pattern, $password_pattern);
}

######################################################################
# Function:     cdat::encode
# Purpose:      Encode passwords.
# Tasks:        Symetric function to encode passwords.
# Input:        Password in clear text
# Output:       Password encoded
######################################################################
sub encode
{
    my ($passwd) = @_;
    my $key = 'ClusterDataAggregationTool';
    my $hidden = '';

    my $pos = 0;
    for (my $i = 0; $i < length($passwd); $i++) {
        vec($hidden, $i, 8) = vec($passwd, $i, 8) ^ vec($key, $pos, 8);
        $pos = ($pos + 1) % length($key);
    }
    return encode_base64($hidden);
}

######################################################################
# Function:     cdat::decode
# Purpose:      Decode passwords.
# Tasks:        Symetric function to decode passwords.
# Input:        Password encoded
# Output:       Password in clear text
######################################################################
sub decode
{
    my ($hidden) = @_;
    my $key = 'ClusterDataAggregationTool';
    my $passwd = '';

    my $pos = 0;
    $hidden = decode_base64($hidden);
    for (my $i = 0; $i < length($hidden); $i++) {
        vec($passwd, $i, 8) = vec($hidden, $i, 8) ^ vec($key, $pos, 8);
        $pos = ($pos + 1) % length($key);
    }
    return $passwd;
}

######################################################################
# Function:     cdat::get_host_db
# Purpose:      Get path to the user host database
# Tasks:        Compute the home directory
#               Compute path of the host database
# Input:        None
# Output:       Path to the host database
######################################################################
sub get_host_db
{
    my $user = odm_get_user();
    return undef unless defined($user);

    my ($name, $pass, $uid, $gid, $quota, $comment,
        $gcos, $dir, $shell, $expire) = getpwnam($user);
    return undef unless defined($dir);

    return "$dir/$HOST_DB";
}

######################################################################
# Function:     cdat::read_host_db
# Purpose:      Read database storing remote host access data
# Tasks:        Read users password for remote access
#               Read host login and password patterns for telnet
# Input:        A hashtable reference to store data
# Output:       None
######################################################################
sub read_host_db
{
    my ($hosts) = @_;
    my $host_db = get_host_db();

    return unless (defined($host_db) && -r "$host_db");

    my $mode = (stat(_))[2];
    if ($mode & (S_IRWXG | S_IRWXO)) {
	printf(catgets(MSG_INVALID_PERM,
	    "File %s has invalid permissions. Please fix it. It should be readable/writable by owner only.\n"),
	    $host_db);
        exit(1);
    }

    my $parser = XML::LibXML->new();
    my $tree = $parser->parse_file("$host_db");
    my $root = $tree->getDocumentElement;

    foreach my $host_elt ($root->getElementsByTagName('host')) {
        my $name = $host_elt->getAttribute('name');
	next if !defined($name);
	my $protocol = $host_elt->getAttribute('protocol');
	next if !defined($protocol);
        $hosts->{$name}{protocol} = $protocol;

        if ($protocol eq 'telnet') {
            my @telnet_elts = $host_elt->getElementsByTagName('telnet');
            if (@telnet_elts) {
                $hosts->{$name}{login_pattern} =
		    $telnet_elts[0]->getAttribute('login_pattern');
                $hosts->{$name}{password_pattern} =
		    $telnet_elts[0]->getAttribute('password_pattern');
            }
	}
        foreach my $user_elt ($host_elt->getElementsByTagName('user')) {
            my $username = $user_elt->getAttribute('name');
	    next if !defined($username);
            my $password = $user_elt->getAttribute('password');
	    next if !defined($password);
	    $hosts->{$name}{users}{$username} = decode($password);
        }
    }
}

######################################################################
# Function:     cdat::write_host_db
# Purpose:      write database storing remote server data
# Tasks:        Store users password for remote access
#               Store host login and password patterns for expect
# Input:        A hashtable reference to stored data
# Output:       None
######################################################################
sub write_host_db
{
    my (%hosts) = @_;
    my $host_db = get_host_db();

    return unless defined($host_db);

    # always overwrite existing file

    chmod(0600, $host_db);
    my $tree = XML::LibXML->createDocument('1.0', cdat::locale_charmap);
    my $root_elt = $tree->createElement('host-list');
    return if !defined($root_elt);
    $tree->setDocumentElement($root_elt);

    foreach my $name (keys(%hosts)) {
        my $host_elt = $tree->createElement('host');
        next if !defined($host_elt);
        $host_elt->setAttribute('name', $name);
	my $protocol = $hosts{$name}{protocol};
        $host_elt->setAttribute('protocol', $protocol);

	if ($protocol eq 'telnet') {
            my $telnet_elt = $tree->createElement('telnet');
            next if !defined($telnet_elt);
	    $telnet_elt->setAttribute('login_pattern',
		$hosts{$name}{login_pattern});
	    $telnet_elt->setAttribute('password_pattern',
		$hosts{$name}{password_pattern});
	    $host_elt->appendChild($telnet_elt);
	}

	my $user_elt;
	my $users = $hosts{$name}{users};
        foreach my $user (keys(%$users)) {
            $user_elt = $tree->createElement('user');
	    next if !defined($user_elt);
            $user_elt->setAttribute('name', $user);
	    my $password = encode($users->{$user});
            $user_elt->setAttribute('password', $password);
            $host_elt->appendChild($user_elt);
        }
        $root_elt->appendChild($host_elt) if defined($user_elt);
    }

    $tree->toFile($host_db, 2);
    chmod(0600, $host_db);
}

######################################################################
# Function:	cdat::get_collecttypes
# Purpose:	Get the list of supported collect types and paths.
# Tasks:	Search for collect types definitions in default dir
#		and in environment variable CDAT_TYPES.
# Input:	None
# Output:	Hash of collect type name -> full path to scripts
######################################################################
sub get_collecttypes
{
    my %collecttypes;
    my @paths;

    # check if the CDAT_TYPES environnement variable is set
    my $env = $ENV{CDAT_TYPES};
    @paths = split(/:/, $env) if defined($env);

    # always prepend "system" scripts
    @paths = (@CDAT_SCRIPTS, @paths);

    foreach my $path (@paths) {
	next if (!opendir(DIR, $path));
	while (defined(my $type = readdir(DIR))) {
	    next if ($type eq '.' || $type eq '..');

	    # it is a collect type if we have a manifest.xml file
	    # and a collect script
	    next if (! -f "$path/$type/$MANIFEST" ||
	    	     ! -x "$path/$type/$type");

	    # keep only the first occurence
	    next if (defined($collecttypes{$type}));

	    $collecttypes{$type} = "$path/$type";
	}
	closedir(DIR);
    }

    return %collecttypes;
}

######################################################################
# Function:     cdat::switch_user
# Purpose:      Switch to the collect user id
# Tasks:        Compute the collect user id
#               Set the effective user id to this id
#               Set environment variable according to this id
# Input:        None
# Output:       None
######################################################################
sub switch_user
{
    my $user = odm_get_user();

    if (!defined($user) || length($user) == 0) {
	printf(STDERR catgets(MSG_LOCAL_USER_NOT_DEF,
	    "The cdat local user is not defined.\n".
	    "Please, run 'cdat init' first.\n"));
	exit(1);
    }

    my ($name, $pass, $uid, $gid, $quota, $comment,
        $gcos, $dir, $shell, $expire) = getpwnam($user);
    if (!defined($uid)) {
        printf(STDERR catgets(MSG_CANNOT_SWITCH_TO_USER,
	    "Cannot switch to user %s.\n"), $user);
        exit(1);
    }

    $> = $uid;
    if ($! != 0) {
        printf(STDERR catgets(MSG_CANNOT_SWITCH_TO_USER,
	    "Cannot switch to user %s.\n"), $user);
        exit(1);
    }
    $ENV{HOME} = $dir;
    $ENV{USER} = $user;
    $ENV{LOGIN} = $user;
    $ENV{LOGNAME} = $user;
}

######################################################################
# Function:	cdat::odm_get
# Purpose:	Perform an ODM query.
# Tasks:	Call odmget and extract the results.
# Input:	Object class, Criteria
# Output:	Array of attribute values or undef
######################################################################
sub odm_get
{
    my ($class, $criteria) = @_;
    my @result;
    my %current = ();

    if (!defined($class)) {
        return undef;
    }

    open(OUTPUT, "$ODMGET -q \"$criteria\" $class |") or
        return undef;

    while (<OUTPUT>) {
        chomp;
        if ($_ eq "$class:") {
            if (%current ne 0) {
                my %item = %current;
                %current = ();
                push(@result, \%item);
            }
        } elsif ($_ =~ m/ = /) {
            my ($name, $value) = split( / = / );
            $name =~ s/^[ \t]*//;
            $value =~ s/"//g;
            next if length($name) == 0;
            $current{$name} = $value;
        }
    }
    if (%current ne 0) {
        my %item = %current;
        push(@result, \%item);
    }

    close OUTPUT;

    return @result;
}

######################################################################
# Function:	cdat::odm_del
# Purpose:	Delete an object from ODM.
# Tasks:	Call odmdel to delete the object.
# Input:	Object class, Criteria
# Output:	Code returned by odmdel or undef
######################################################################
sub odm_del
{
    my ($class, $criteria) = @_;

    if (!defined($criteria) || !defined($class)) {
        return undef;
    }

    my $result = `$ODMDEL -o "$class" -q "$criteria"`;

    $result =~ s/^0518-307 odmdelete: //;
    $result =~ s/ objects deleted//;

    return $result;
}

######################################################################
# Function:	cdat::odm_add
# Purpose:	Add an object into ODM.
# Tasks:	Call odmadd to add the object.
# Input:	Object class, Attribute
# Output:	None
######################################################################
sub odm_add
{
    my ($class, %attributes) = @_;

    open ODMDB, "|$ODMADD";

    print(ODMDB "$class:\n");
    while (my($name, $value) = each(%attributes)) {
        print(ODMDB "$name=\"$value\"\n");
    }
    close ODMDB;
}

######################################################################
# Function:	cdat::odm_change
# Purpose:	Change an object in ODM.
# Tasks:	Call odmchange to change the object.
# Input:	Object class, Criteria, Attribute
# Output:	None
######################################################################
sub change
{
    my ($class, $criteria, %attributes) = @_;

    open ODMDB, "|$ODMCHG -o \"$class\" -q \"$criteria\"";

    while (my($name, $value) = each(%attributes)) {
        print(ODMDB "$name=\"$value\"\n");
    }
    close ODMDB;
}

1;