Inettab

From Qmailwiki
Jump to: navigation, search

Contents

InetTab

Inettab is yet another way to manage services on a *nix OS but with some features we needed in a clustered environment.

Features

We needed these features, so they are included in inettab

  • authenticated management protocol (start/stop processes, status, etc)
  • service available on tcp/ip address and port with IP based access control lists.
  • Ability to set environment variables.
  • Automated restart of services that die.
  • Ability to start a daemon and direct standard out and standard error to a second logging daemon.
  • Direct email notification (connects directly to the To: MX server) on events.
  • A simple php method to send authenticated commands to manage services such as start, stop, restart and get status.
  • Ability to encrypt the client <-> inettab communication with standard SSL libraries.

Comparison with init(8)

Init only supports the automated restart of a service.

Comparision with djb's svscan

Service scan has all of the above features except for the inetnet command feature. A simple tcpserver type program could be written to use the svc program to support any of the above features. svscan has a different process parenting method which uses more processes that inettab. svcscan runs as a single daemon normally started by init(8). It starts a supervise process for every service it finds in a control directory. If the service also has a logging process it starts a second supervise process along with the logging process. Inetinit eliminates the supervise level.

Process Ownership

Processes are started with a normal fork() call followed by an execve() with the appropriate environment settings. Before the fork() a pipe() is created. The fork/execve process has it's standard in taken from the inettab daemon. Standard out is directed to the pipe. Standard error is reconnected to standard output through the pipe.

The logging process is then forked and has it's standard input connected to the other side of the pipe. Various house keeping is done along with saving the process Id's of the daemon and logging processes.

Process ID's and multilog style startup lines

Take a look at this standard multilog startup line

 some-daemon | /usr/local/bin/setuidgid qmaill /usr/local/bin/multilog t n20 s1000000 /var/log/qmail

The inettab daemon would start everything after the | symbol. Which means it will exec the setuidgid program with the rest of the line as arguments. The setuidgid program sets the ownership of the program then execs multilog with the rest of the line as arguments. The process ID that inettab gets will be the same for the setuidgid program and for the multilog program. Even though a second program was started it still has the same process ID. Which means if inettab needs to kill the multilog program it can use the saved process ID from when it forked the logging program.

Note that the same logic applies to starting programs with the softlimit program. The process ID of the first fork will be the same all the way down the chain of programs until the daemon is running. The reason why is that each program in the chain does an exec() call and not a fork() and exec(). An exec() replaces the current running program with new code. A fork creates a new process with a new process ID. The chained programs just need to do their bit of work then replaces themselves with the next program in the chain with it's arguments.

Dead daemon or logging programs

When one of the forked programs, also known as a child, dies, the operating system generates a "sig child" signal. The inettab daemon sets a "sig child" handler that handles when a child dies. If we want, this can include killing and restarting the daemon and logging programs. It could also notify someone. If inettab does not do a wait() or waitpid() to "reap" the "dieing child" then it turns into child turns into a "zombie". Zombie processes are like the living dead. They stay around but can not be killed until the parent process wait()'s or the parent dies and the operating system reaps the parent and all the kids including the zombies.

Setting Environment Variables

One nice thing that init lacks but svscan includes is passing information to the daemon processes using environment variables. inettab does this by reading the configuration file and parsing one or more environment lines then building the proper array to pass to the execve() system call.

PHP Client Code

It is probably worth while for people writing PHP front ends to inettab to go through some sample (and working) code.

The code uses the fsockopen() php function so we can easily process the line based communication protocol. The fgets function will read from the network and return one line, automatically handling all the buffering issues. It is the standard unix stream protocol.

Connect to the server

function  rservice_login($inettab_ip, $port, $user, $pass)
{
 /* open connection */
 $fp = fsockopen($inettab_ip, $port, $errno, $errstr, 15);
 if ( !$fp ) {
  print("rservice_login: connect failed: $errstr ($errno)\n");
  return(0);
 }

 /* read connect message */
 if ( ($buffer = fgets($fp, 4096)) == FALSE ) {
  fclose($fp);
  print("rservice_login: fgets 1 failed\n");
  return(0);
 }

 /* authenticate */
 $str = sprintf("login %s %s\n", $user, $pass);
 $ret = fwrite($fp, $str);
 if ($ret == FALSE) {
    fclose($fp);
    print("rservice_login: fwrite 1 failed\n");
    return(0);
 }

 /* read single or multi line response */
 $virgin = 1;
 while (!feof($fp)) {

   if ( ($buffer = fgets($fp, 4096)) == FALSE ) {
    fclose($fp);
    print("rservice_login: fgets 2 failed\n");
    return(0);
   }

   /* first time through check for single line response */
   if ( $virgin == 1 ) {
     $virgin = 0;
     /* multiline */
     if ( strncmp( $buffer, "+OK+", 4) == 0 ) {
       continue;
     /* single line error */
     } else if ( strncmp( $buffer, "-ERR", 4) == 0 ) {
       printf("rservice_login: login failed: %s\n", $buffer);
       return(0);
     /* single line success */
     } else if ( strncmp( $buffer, "+OK", 3) == 0 ) {
       return(1);
     /* unknown server response */
     } else  {
       print("rservice_login: unknown response\n");
       fclose($fp);
       return(0);
     }
   }

   /* last line of multiline response has just a single .
    * a single line with a . gets encoded as two .'s
    */
   if ($buffer[0] == '.' && $buffer[1] != '.' ) break;
 }

 /* return the FILE socket */
 return($fp);
}

Function to start/stop/restart a service

function rservice( $inettab_ip, $port, $user, $pass, $process, $command )
{
 /* authenticate with server */
 if ( ($fp=rservice_login($inettab_ip, $port, $user, $pass)) == 0 ) return(0);

 /* send the command */
 $str = sprintf("%s %s\n", $command, $process);
 $ret = fwrite($fp, $str);
 if ($ret == FALSE) {
    fclose($fp);
    print("rservice: fwrite 1 failed\n");
    return(0);
 }

 /* read the response */
 if ( ($buffer = fgets($fp, 4096)) == FALSE ) {
  fclose($fp);
  print("rservice: fgets 1 failed\n");
  return(0);
 }

 /* close the connection */
 fclose($fp);

 /* return success or failure */
 if ( strcmp($buffer, "+OK") == 0 ) return(1);
 return(0);

}

Function to get the Status

function rservice_status( $inettab_ip, $port, $user, $pass )
{
 /* authenticate with server */
 if ( ($fp=rservice_login($inettab_ip, $port, $user, $pass)) == 0 ) return(0);

 /* send status command */
 $ret = fwrite($fp, "status\n");
 if ($ret == FALSE) {
    fclose($fp);
    print("rservice_status: fwrite 1 failed\n");
    return(0);
 }

 /* read response */
 $i = 0;
 $virgin = 1;
 $cstatus = array( );
 while (!feof($fp)) {

   if ( ($buffer = fgets($fp, 4096)) == FALSE ) {
    fclose($fp);
    print("rservice_status: fgets 1 failed\n");
    return(0);
   }

   /* first time through */
   if ( $virgin == 1 ) {
     $virgin = 0;

     /* multiline okay */
     if (strncmp($buffer,"+OK+",4 ) == 0 ) {
       continue;

     /* error */
     } else if (strncmp($buffer,"-ERR",4 ) == 0 ) {
       fclose($fp);
       return(0);
     /* single line okay */
     } else if (strncmp($buffer,"+OK",3 ) == 0 ) {
       fclose($fp);
       return(1);

     /* what the hell */
     } else {
       print("rservice_status: unknown response\n");
       fclose($fp);
       return(1);
     }

   }

   /* last line has just a single dot */
   if ($buffer[0] == '.' && $buffer[1] != '.' ) break;

   /* parse the status information */
   if ( ($processn = strtok($buffer, " \t\r\n")) == false ) continue;
   if ( ($tstatus = strtok(" \t\r\n")) == false ) continue;

   $cstatus[$i]['process'] = $processn;
   $cstatus[$i]['status'] = $tstatus;
   $i++;
 }

 /* close the connection */
 fclose($fp);

 /* return the status array */
 return($cstatus);

}


Sample usage of functions

 /* stop qmail */
 $result = rservice("127.0.0.1", "59", "postmaster@example.com", "test", "stop", "qmail");
 if ( $result == 1 ) printf("qmail stopped\n");
 else printf("error on stopping qmail\n");

 /* start qmail */
 $result = rservice("127.0.0.1", "59", "postmaster@example.com", "test", "start", "qmail");
 if ( $result == 1 ) printf("qmail started\n");
 else printf("error on starting qmail\n");

 /* get and print the status */
 $stat_result = rservice_status( "127.0.0.1", "59", "postmaster@example.com", "test" );
 foreach ( $stat_result as &s ) {
   printf("service: %s status: %s\n", s.process, s.status);
 }
Personal tools